WTF Solidity 102
15. Overloading
Overloading là tính năng cho phép nhiều hàm có tên giống nhau nhưng khác về tham số.
function saySomething() public pure returns(string memory) {
return("Nothing");
}
function saySomething(string memory something) public pure returns(string memory) {
return(something);
}
Lưu ý: Solidity không cho phép tính các modifier có cùng tên
Hai hàm có thể giống nhau hoàn toàn về tên và tham số đầu vào, nhưng khác nhau ở kiểu trả về.
function f(uint8 _in) public pure returns (uint8 out) {
out = _in;
}
function f(uint256 _in) public pure returns (uint256 out) {
out = _in;
}
16. Library (thư viện)
Thư viện là một dạng hợp đồng đặc biệt, giúp các nhà phát triển có thể tái sử dụng các đoạn mã đã được viết trước đó để tích hợp.
Thư viện có một số điểm khác biệt so với các hợp đồng thông thường: 1.Các biến trạng thái phải là hằng số 2. Không có tính năng kế thừa 3. Không thể nhận ETH 4. Không thể hủy (Self Destruct như contract thường)
String library contract
Chúng ta cùng xem qua 1 ví dụ về thư viện String
, giúp chuyển đổi từ kiểu uint256
sang string
.
library Strings {
bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";
/**
* @dev Chuyển số dạng unit256 sang dạng chuỗi string
*/
function toString(uint256 value) public pure returns (string memory) {
// Inspired by OraclizeAPI's implementation - MIT licence
// https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol
if (value == 0) {
return "0";
}
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}
/**
* @dev Chuyển số dạng unit256 sang dạng chuỗi string (hệ cơ số 16)
*/
function toHexString(uint256 value) public pure returns (string memory) {
if (value == 0) {
return "0x00";
}
uint256 temp = value;
uint256 length = 0;
while (temp != 0) {
length++;
temp >>= 8;
}
return toHexString(value, length);
}
/**
* @dev Chuyển số dạng unit256 sang dạng chuỗi string (hệ cơ số 16) (với độ dài cố định)
*/
function toHexString(uint256 value, uint256 length) public pure returns (string memory) {
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = _HEX_SYMBOLS[value & 0xf];
value >>= 4;
}
require(value == 0, "Strings: hex length insufficient");
return string(buffer);
}
}
Sử dụng thư viện với từ khóa using
// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.19;
import "./Strings.sol";
contract Test {
using Strings for uint256;
function getString1(uint256 _number) public pure returns(string memory) {
return _number.toHexString();
}
}
Gọi trực tiếp bằng tên contract
function getString2(uint256 _number) public pure returns(string memory){
return Strings.toHexString(_number);
}
Thử deploy và kiểm tra, kết quả là 2 hàm cho đầu ra hoàn toàn như nhau.
17. Import
Import là một tính năng cơ bản trong Solidity, hỗ trợ việc thêm các file từ bên ngoài bằng nhiều kiểu khác nhau.
- Import trực tiếp bằng đường dẫn thư mục (đối với các file nằm trong mã nguồn)
Cây thư mục
├── Import.sol
└── Yeye.sol
// Import
import './Yeye.sol';
- Import bằng URL
// Import by URL
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol';
- Import qua package npm
import '@openzeppelin/contracts/access/Ownable.sol';
- Import tên contract từ file (file có thể có nhiều contract)
import {Yeye} from './Yeye.sol';
18. Fallback
Solidity có 2 hàm đặc biệt là receive()
và fallback()
, chủ yếu được sử dụng trong 2 TH:
- Nhận ETH
- Thực thi khi lời gọi đến hợp đồng không chỉ định gọi đến 1 hàm cụ thể nào
Nhận ETH: receive()
Khi gửi ETH từ 1 tài khoản vào hợp đồng, hàm receive()
sẽ được gọi.
event Received(address Sender, uint Value);
/*
- Không định nghĩa với từ khóa function, không thể có tham số
- Không có kiểu dữ liệu trả về và 1 hợp đồng chỉ có tối đa 1 hàm receive
- Phải là external và có từ khóa payable
*/
receive() external payable {
emit Received(msg.sender, msg.value);
}
Fallback function: fallback()
Hàm fallback()
được gọi khi lời gọi đến hợp đồng ko gọi bất cứ hàm nào, hoặc nhận ETH nếu không có hàm receive()
ở đó.
event fallbackCalled(address Sender, uint Value, bytes Data);
/*
- Không định nghĩa với từ khóa function
- Phải là external
- Nếu muốn nhận ETH thì cần thêm từ khóa payable
*/
fallback() external payable {
emit fallbackCalled(msg.sender, msg.value, msg.data);
}
Sự khác nhau giữa receive()
và fallback()
Execute fallback() or receive()?
Nhận ETH
|
msg.data rỗng hay không?
/ \
Có Không
/ \
Có receive()? fallback()
/ \
Có Không
/ \
receive() fallback()
19. Gửi ETH
Có 3 cách để gửi ETH trên Solidity đó là: transfer()
, send()
và call()
. Trong đó call()
được khuyến nghị sử dụng nhất.
Trước tiên, ta viết 1 contract nhận ETH
contract ReceiveETH {
// Nhận ETH, log amount và gas
event Log(uint amount, uint gas);
receive() external payable {
emit Log(msg.value, gasleft());
}
// Kiểm tra số dư ETH của contract
function getBalance() view public returns (uint) {
return address(this).balance;
}
}
transfer
- gas limit là 2300, vừa đủ để gửi. Nhưng sẽ không đủ nếu hàm
receive()
hayfallback()
của contract nhận có thêm logic tiêu thụ gas. - Nếu chuyển không thành công thì giao dịch sẽ bị revert
// sending ETH with transfer()
// Đơn vị của amount là wei
function transferETH(address payable _to, uint256 amount) external payable{
_to.transfer(amount);
}
send
- gas limit 2300 như
transfer()
- Giao dịch cũng sẽ bị revert nếu chuyển không thành công
- Giá trị trả về kiểu
boolean
// sending ETH with send()
// Đơn vị của amount là wei
function sendETH(address payable _to, uint256 amount) external payable{
bool success = _to.send(amount);
if(!success){
revert SendFailed();
}
}
call
- gas limit không bị giới hạn. Có thể gửi đến các contract mà hàm fallback có nhiều logic.
call()
lỗi thì sẽ không revert- Giá trị trả về kiểu (bool, data)
// sending ETH with call()
function callETH(address payable _to, uint256 amount) external payable{
// check result of call(),revert with error when failed
(bool success, ) = _to.call{value: amount}("");
if(!success){
revert CallFailed();
}
}
20. Gọi contract
Chúng ta sẽ tìm hiểu cách tương tác với 1 contract đã deployed như thế nào trong Soldity. Trước tiên ta có OtherContract
.
contract OtherContract {
uint256 private _x = 0
event Log(uint amount, uint gas);
function getBalance() view public returns(uint) {
return address(this).balance;
}
// Thay đổi giá trị của x, có thể nhận ETH gửi vào
function setX(uint256 x) external payable {
_x = x;
// emit Log event when receiving ETH
if(msg.value > 0){
emit Log(msg.value, gasleft());
}
}
function getX() external view returns(uint x){
x = _x;
}
}
Các cách mà chúng ta có thể tương tác với OTherContract
từ 1 contract khác như sau
1. Truyền địa chỉ contract
function callSetX(address _Address, uint256 x) external{
OtherContract(_Address).setX(x);
}
2. Truyền biến contract
function callGetX(OtherContract _Address) external view returns (uint x) {
x = _Address.getX();
}
3. Tạo biến contract
function callGetX(OtherContract _Address) external view returns (uint x) {
x = _Address.getX();
}
4. Gọi hàm đồng thời chuyển ETH
function setXTransferETH(address otherContract, uint256 x) payable external {
OtherContract(otherContract).setX{value: msg.value}(x);
}
21. Call
Ở mục 19. Send ETH, chúng ta đã có đề cập đến lệnh call
. Và giờ chúng ta sẽ tìm hiểu sâu hơn 1 chút về call
trong Solidity.
call
là một chức năng ở mức thấp (low-level), được sử dụng để gọi đến các hợp đồng khác. Giá trị trả về của call
có dạng (bool, data) lần lượt là trạng thái và dữ liệu trả về từ lời gọi.
call
được khuyến nghị sử dụng để gửi ETH khi contract được gọi đến có các hàmreceive
hayfallback
call
không được khuyến khích dùng để tương tác với các contract khác do tiềm ẩn rủi ro bảo mật.
Cú pháp
targetContractAddress.call(binary code);
/* binary code có dạng: abi.encodeWithSignature("function signature", giá trị các tham số)
VD: abi.encodeWithSignature("f(uint256,address)", _x, _addr)
*/
// Khi gửi kèm cả ETH thì ta có cú pháp như sau:
contractAdress.call{value:ETH value, gas:gas limit}(binary code);
Ví dụ
Chúng ta có OtherContract
là contract mà chúng ta sẽ gọi đến
contract OtherContract {
uint256 private _x = 0; // state variable x
// Receiving ETH event, log the amount and gas
event Log(uint amount, uint gas);
fallback() external payable{}
// get the balance of the contract
function getBalance() view public returns(uint) {
return address(this).balance;
}
// set the value of _x, as well as receiving ETH (payable)
function setX(uint256 x) external payable{
_x = x;
// emit Log event when receiving ETH
if(msg.value > 0){
emit Log(msg.value, gasleft());
}
}
// read the value of x
function getX() external view returns(uint x){
x = _x;
}
}
Gọi hàm setX
function callSetX(address payable _addr, uint256 x) public payable {
// gọi setX và gửi thêm ETH
(bool success, bytes memory data) = _addr.call{value: msg.value}(
abi.encodeWithSignature("setX(uint256)", x)
);
emit Response(success, data); //emit event
}
Kết quả trả về : (true, 0x)
, trạng thái giao dịch thành công, hàm setX
không có kết quả trả về nên data sẽ là 0x
.
Gọi hàm getX
function callGetX(address _addr) external returns(uint256){
// call getX()
(bool success, bytes memory data) = _addr.call(
abi.encodeWithSignature("getX()")
);
emit Response(success, data); //emit event
return abi.decode(data, (uint256));
}
Do đã gán x = 5 ở phần trên, data trả về có dạng 0x0000000000000000000000000000000000000000000000000000000000000005
, sau khi abi.decode thì nó chính bằng 5, giá trị của x.
Gọi 1 hàm không tồn tại
function callNonExist(address _addr) external{
// Gọi 1 hàm tên là foo
(bool success, bytes memory data) = _addr.call(
abi.encodeWithSignature("foo(uint256)")
);
emit Response(success, data); //emit event
}
Mặc dù hàm foo
không tồn tại nhưng kết quả trả về vẫn là true
do lời gọi đã được chuyển tiếp đến hàm fallback
của OtherContract
.
22. DelegateCall
Khi 1 contract A gọi hàm của contract C thông qua contract B, hàm được thực thi trên contract C sẽ thay đổi biến, trạng thái tại contract C, msg.sender sẽ là contract B. Rất cơ bản và dễ hiểu
Với delegateCall
, msg.sender sẽ là contract A chứ không phải contract B. Hàm được gọi trên contrac C nhưng sẽ thay đổi trạng thái ở contract B, nghe hơi khó hiểu. Chúng ta cùng xem ngay ví dụ dưới đây
Ví dụ
Ta có contract C:
// Target contract C
contract C {
uint public num;
address public sender;
// Hàm gán giá trị của num và sender
function setVars(uint _num) public payable {
num = _num;
sender = msg.sender;
}
}
Tiếp đó là contract B:
contract B {
uint public num;
address public sender;
// gọi hàm setVars() của contract C with `call()`, sẽ thay đổi biến trạng thái trên C
function callSetVars(address _addr, uint _num) external payable{
// call setVars()
(bool success, bytes memory data) = _addr.call(
abi.encodeWithSignature("setVars(uint256)", _num)
);
}
// gọi setVars() với delegatecall, sẽ thay đổi biến trạng thái trên B
function delegatecallSetVars(address _addr, uint _num) external payable{
// delegatecall setVars()
(bool success, bytes memory data) = _addr.delegatecall(
abi.encodeWithSignature("setVars(uint256)", _num)
);
}
}
- Deploy B và C
- Kiểm tra giá trị các biến ban đầu trên 2 contract
- Gọi
callsetVars
với tham số là địa chỉ của C và 10 4. Sau khi thực thicallsetVars
giá trị num trên C bằng 10, sender là địa chỉ của B 5. GọidelegateCallSetVars
với tham số là địa chỉ của C và 100 6. Giá trị biến num và sender trên B đã thay đổi. num bằng 100 và sender là địa chỉ ví trên Remix
Ứng dụng của delegateCall
- Proxy Contract: Proxy contract có trách nhiệm xử lý các biến trạng thái và lưu địa chỉ của hợp đồng chứa logic chính. Mỗi khi cần nâng cấp hay thay đổi về logic ứng dụng, chỉ cần deploy contract mới và thay đổi giá trị địa chỉ trên proxy.
- EIP-2535 Diamonds: Là một đề xuất xây dựng các hợp động theo kiểu mô-đun, có khả năng mở rộng cao. Các bạn có thể tìm hiểu kỹ hơn về EIP-2535 Diamonds tại đây
23. Create
Trong Ethereum, không chỉ tài khoản ví (Externally-owned account) có thể tạo mới hợp đồng mà chính hợp đồng thông minh cũng có thể tạo ra các hợp đồng mới. Ví dụ điển hình là contract Factory
trong mô hình Uniswap có thể tạo ra nhiều contract Pair
là các cặp token có thể trao đổi.
create và create2
Có 2 cách để tạo mới contract là create
và create2
. Ở mục này chúng ta sẽ giải thích create
trước khi nói về create2
ở mục tiếp theo.
/* Sử dụng create rất đơn giản với từ khóa `new`
`Contract` là tên contract sẽ tạo mới
- Nếu constructor có từ khóa payable thì ta có thể truyền _value khi tạo mới contract để chuyển 1 lượng ETH tùy ý vào đó
- param là các đối số có trong constructor của Contract
*/
Contract x = new Contract{value: _value}(params)
Ví dụ Uniswap
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
// Contract Pair giúp quản lý cặp token, thanh khoản, swap
contract Pair{
address public factory; // factory contract address
address public token0; // token1
address public token1; // token2
constructor() payable {
factory = msg.sender;
}
// Chỉ được gọi 1 lần sau khi deploy
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN');
token0 = _token0;
token1 = _token1;
}
}
// Contract PairFactory có chức năng tạo mới cũng như quản lý các Pair contract
contract PairFactory{
// mapping lưu trữ địa chỉ các Pair đã tạo và các token của Pair đó
mapping(address => mapping(address => address)) public getPair;
// Lưu trữ địa chỉ của tất cả các Pair contract đã tạo
address[] public allPairs;
function createPair(address tokenA, address tokenB) external returns (address pairAddr) {
// Tạo mới 1 contract Pair
Pair pair = new Pair();
// Gọi hàm init
pair.initialize(tokenA, tokenB);
// Cập nhật thông tin về Pair mới
pairAddr = address(pair);
allPairs.push(pairAddr);
getPair[tokenA][tokenB] = pairAddr;
getPair[tokenB][tokenA] = pairAddr;
}
}
24. Create2
create2
giúp chúng ta có thể dự đoán trước địa chỉ của smart contract trước khi được triển khai.
Cách create
tính toán địa chỉ
/*
- creator's address là địa chỉ deploy contract
- nonce là số nonce của creator
*/
new address = hash(creator's address, nonce)
địa chỉ của người tạo sẽ không thay đổi, nhưng nonce có thể thay đổi theo thời gian, vì vậy rất khó để dự đoán địa chỉ của hợp đồng được tạo bằng CREATE.
Cách create2
tính toán địa chỉ
// salt là giá trị được truyền vào bởi creator
// bytecode là mã bytecode của contract
new address = hash("0xFF", creator's address, salt, bytecode)
CREATE2 không phụ thuộc vào nonce mà là các tham số có thể biết trước được giá trị như salt
hay bytecode
, do đó dễ dự đoán địa chỉ của contract mới hơn.
Cú pháp của create2
Contract x = new Contract{salt: _salt, value: _value}(params)
Ví dụ
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract Pair {
address public factory; // factory contract address
address public token0; // token1
address public token1; // token2
constructor() payable {
factory = msg.sender;
}
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN');
token0 = _token0;
token1 = _token1;
}
}
contract PairFactory2 {
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
function createPair2(address tokenA, address tokenB) external returns (address pairAddr) {
require(tokenA != tokenB, 'IDENTICAL_ADDRESSES');
// Sắp xếp thứ tự cặp token (Đảm bảo cặp A-B và B-A là một)
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
// Tính toán salt từ địa chỉ 2 token
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
// Deploy contract mới với create2
Pair pair = new Pair{salt: salt}();
pair.initialize(tokenA, tokenB);
// Update address map
pairAddr = address(pair);
allPairs.push(pairAddr);
getPair[tokenA][tokenB] = pairAddr;
getPair[tokenB][tokenA] = pairAddr;
}
// Tính toán địa chỉ `Pair` contract trước
function calculateAddr(address tokenA, address tokenB) public view returns(address predictedAddress){
require(tokenA != tokenB, 'IDENTICAL_ADDRESSES');
// Sắp xếp
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
// Tính salt
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
// Tính toán contract address
predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(type(Pair).creationCode)
)))));
}
}
25. DeleteContract
selfdestruct
là câu lệnh xóa contract và chuyển lượng ETH còn lại (nếu có) cho 1 địa chỉ khác.
Ví dụ
contract DeleteContract {
uint public value = 10;
constructor() payable {}
receive() external payable {}
function deleteContract() external {
// xóa contract và gửi tiền đến địa chỉ msg.sender
selfdestruct(payable(msg.sender));
}
function getBalance() external view returns(uint balance){
balance = address(this).balance;
}
}
- Do tính chất khá "nguy hiểm" của
selfdestruct
, khi viết hàm xóa contract nên sử dụng thêm modifier nhưonlyOwner
để giới hạn địa chỉ có thể thực thi hàm - Khi contract bị xóa, mọi tương tác đến contract đều trả về 0.
26. ABI encode và decode
ABI
(Application Binary Interface) là một tiêu chuẩn giúp tương tác với các hợp đồng thông minh.
Trong Solidity, cách mã hóa ABI có 4 hàm:
abi.encode
abi.encodePacked
abi.encodeWithSignature
abi.encodeWithSelector
Trong khi đó, chỉ có 1 hàm là abi.decode
để giải mã từ abi.encode
ABI encode
uint x = 10;
address addr = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71;
string name = "0xAA";
uint[2] array = [5, 6];
abi.encode
function encode() public view returns(bytes memory result) {
result = abi.encode(x, addr, name, array);
}
Với abi.encode
, mỗi tham số sẽ có không gian 32 bytes để biểu diễn.
Như đoạn code trên, kết quả cho ra sẽ là:
0x000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
- x convert thành
0x000000000000000000000000000000000000000000000000000000000000000a
- addr thành
0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c71
...
abi.encodePacked
function encodePacked() public view returns(bytes memory result) {
result = abi.encodePacked(x, addr, name, array);
}
Kết quả encode sẽ là:
0x000000000000000000000000000000000000000000000000000000000000000a7a58c0be72be218b41c608b7fe7c5bb630736c713078414100000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000006
abi.encodePacked
cho kết quả ngắn gọn hơn abi.encode
do sử dụng ít không gian cho việc mã hóa hơn. Ví dụ như kiểu uint
chỉ cấp cho không gian 1 bytes.
abi.encodePacked
sẽ được sử dụng trong các trường hợp muốn tiết kiệm không gian tính toán và không phải tương tác với contract. Như tính mã băm của 1 số dữ liệu là TH phổ biến hay sử dụng.
abi.encodeWithSignature
function encodeWithSignature() public view returns(bytes memory result) {
result = abi.encodeWithSignature("foo(uint256,address,string,uint256[2])", x, addr, name, array);
}
}
Kết quả encode sẽ là:
0xe87082f1000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000007a58c0be72be218b41c608b7fe7c5bb630736c7100000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000043078414100000000000000000000000000000000000000000000000000000000
Sẽ có thêm phần đầu là function signature (0xe87082f1000000000000000000000000000000000000000000000000000000000000000), các tham số khác được mã hóa giống như abi.encode
.
abi.encodeWithSelector
function encodeWithSelector() public view returns(bytes memory result) {
result = abi.encodeWithSelector(bytes4(keccak256("foo(uint256,address,string,uint256[2])")), x, addr, name, array);
}
Giống như abi.encodeWithSignature
nhưng phần đầu function selector
sẽ là 4 bytes đầu của function signature
ABI decode
abi.decode
là hàm ngược của abi.encode
function decode(bytes memory data) public pure returns(uint dx, address daddr, string memory dname, uint[2] memory darray) {
(dx, daddr, dname, darray) = abi.decode(data, (uint, address, string, uint[2]));
}
27. Hash
Hàm băm là một khái niệm mật mã. Có thể chuyển đổi một thông điệp với độ dài tùy ý thành giá trị có độ dài nhất định.
Một thuật toán băm tốt sẽ cần thỏa mãn các tính chất sau:
- Một chiều (one-way): Việc đảo ngược lại giá trị băm là bất khả thi.
- Tính nhạy cảm (Sensitivity): Một thay đổi nhỏ của đầu vào sẽ cho ra giá trị băm khác.
- Khả năng chống va chạm: Với x # y, rất khó để hash(x) = hash(y)
Hàm băm keccak256
trong Solidity đảm bảo được các yếu tố đó
hash = keccak256(data);
28. Selector
Khi tra cứu các giao dịch trên etherscan chẳng hạn, ta sẽ bắt gặp 1 trường thông tin là input
, 4 bytes đầu tiên của input gọi là function selector
Lưu ý: Với các giao dịch không gọi hàm như chuyển ETH (trường input sẽ có giá trị 0x
)
0x095ea7b3
chính là function selector
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract Selector {
// output selector
// "mint(address)": 0x6a627842
// 4 bytes đầu tiên khi băm function signature với keccak256 gọi là `method id`
function mintSelector() external pure returns (bytes4 mSelector) {
return bytes4(keccak256("mint(address)"));
}
// use selector để gọi hàm
function callWithSignature() external returns (bool, bytes memory) {
// use `abi.encodeWithSelector` to pack and encode the `mint` function's `selector` and parameters
(bool success, bytes memory data) = address(this).call(
abi.encodeWithSelector(
0x6a627842, // method id
"0x2c44b726ADF1963cA47Af88B284C06f30380fC78"// tham số truyền vào
)
);
return (success, data);
}
}
Chúng ta có thể dùng function selector kết hợp với lệnh call
để gọi hàm trong Solidity.
29. Try Catch
Tính năng try-catch bắt đầu có từ phiên bản Solidity 0.6. Try-catch chỉ có thể sử dụng trong các hàm external
hoặc hàm khởi tạo (constructor).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract OnlyEven{
constructor(uint a){
require(a != 0, "invalid number");
assert(a != 1);
}
function onlyEven(uint256 b) external pure returns(bool success){
require(b % 2 == 0, "Ups! Reverting");
success = true;
}
}
contract TryCatch {
event SuccessEvent();
event CatchEvent(string message);
event CatchByte(bytes data);
// tạo object của OnlyEven contract
OnlyEven even;
constructor() {
even = new OnlyEven(2);
}
// sử dụng try-catch khi gọi đến hàm ở contract khác
// gọi hàm onlyEven, nếu thành công thì emit SuccessEvent
// Thất bại thì emit CatchEvent
function execute(uint amount) external returns (bool success) {
try even.onlyEven(amount) returns(bool _success){
// if call succeeds
emit SuccessEvent();
return _success;
} catch Error(string memory reason){
// if call fails
emit CatchEvent(reason);
}
}
// sử dụng try-catch khi tạo mới instance contract OnlyEven
// executeNew(0) sẽ emit `CatchEvent` (vì dính require có message)
// executeNew(1) sẽ emit `CatchByte` (vì dính assert)
// executeNew(2) thành công emit `SuccessEvent`
function executeNew(uint a) external returns (bool success) {
try new OnlyEven(a) returns(OnlyEven _even){
// if call succeeds
emit SuccessEvent();
success = _even.onlyEven(a);
} catch Error(string memory reason) {
// sẽ bắt các lỗi dạng như revert("reasonString") and require(false, "reasonString")
emit CatchEvent(reason);
} catch (bytes memory reason) {
// sẽ bắt các lỗi dạng như assert()
emit CatchByte(reason);
}
}
}
Nguồn
https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en
All rights reserved