Solidity là ngôn ngữ chủ đạo trong việc phát triển smart contract trên Ethereum cũng như các nền tảng blockchain EVM khác. ài viết này sẽ giúp các bạn nắm bắt được những khái niệm cơ bản nhất của Solidity, làm nền tảng để tiếp cận các khái niệm nâng cao hơn ở phần sau.
1. Hello Web3
Chúng ta sẽ dùng công cụ IDE online Remix để viết, biên dịch và triển khai smart contract Solidity. Với Remix, chúng ta sẽ không cần cài đặt, cấu hình hay chạy các câu lệnh phức tạp ở máy local. Điều này rất thân thiện cho người mới bắt đầu
1. Tạo File
Chúng ta truy cập Remix và tạo 1 file mới tên là HelloWeb3.sol
2. Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4; contract HelloWeb3 { string public hello = "Hello Web3!";
}
-
- Dòng đầu tiên là chú thích, và chúng ta sẽ viết về giấy phép phần mềm (license) được sử dụng trong đoạn mã này (ở đây là MIT). Nếu không có dòng này thì khi biên dịch sẽ có cảnh báo nhưng chương trình vẫn chạy được.
-
- Dòng thứ hai khai báo phiên bản solidity được sử dụng. Dòng này có nghĩa rằng code sẽ không được phép biên dịch với phiên bản trình biên dịch nhỏ hơn 0.8.4 hoặc lớn hơn hoặc bằng 0.9.0 (điều kiện được cung cấp bởi ^). Câu lệnh solidity kết thúc bằng dấu chấm phẩy ;
-
- Dòng thứ ba khai báo tên của smart contract (ở đây là HelloWeb3). Bên trong cặp dấu ngoặc {} sẽ chứa logic của smart contract.
-
- Khai báo biến hello dạng chuỗi (string) và gán bằng "Hello Web3!"
3. Biên dịch và triển khai
- Ấn Ctr + S để biên dịch code. Sau đó chuyển sang tab deploy.
- Ấn nút Deploy để triển khai
- Ở phần Deployed Contracts ta sẽ tương tác được với smart contract vừa deploy
- Ấn vào hello ta sẽ thấy giá trị "Hello Web3!" hiển thị
Trong trường hợp này, chúng ta đã deploy smart contract lên môi trường máy ảo của Remix, rất nhanh và tiện lợi. Ngoài ra, Remix còn cho phép kết nối đến các mạng blockchain mainnet, testnet thông qua ví (điển hình là Metamask ... ). Còn nhiều tính năng hay ho khác, các bạn từ từ tìm hiểu thêm nhé
2. Value Types
Các kiểu dữ liệu trong Solidity có thể chia thành 4 loại
- Value Type : Bao gồm Boolean, Integer, Address v.v... Các biến kiểu này được gán giá trị trực tiếp.
- Kiểu tham chiếu (Reference Type) : Gồm mảng và struct
- Ánh xạ (Mapping Type) : Mapping
- Hàm (Function Type)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
contract ValueTypes{ // Kiểu Boolean bool public _bool = true; // Các phép logic với biến boolean bool public _bool1 = !_bool; bool public _bool2 = _bool && _bool1; bool public _bool3 = _bool || _bool1; bool public _bool4 = _bool == _bool1; bool public _bool5 = _bool != _bool1; // Các kiểu số int public _int = -1; uint public _uint = 1; uint256 public _number = 20220330; // 1 số phép toán uint256 public _number1 = _number + 1; // +,-,*,/ uint256 public _number2 = 2**2; // Lũy thừa uint256 public _number3 = 7 % 2; // modulo bool public _numberbool = _number2 > _number3; // So sánh (trả về boolean) // kiểu address lưu trữ 20 byte dữ liệu (định dạng địa chỉ trên Ethereum) address public _address = 0x7A58c0Be72BE218B41C608b7Fe7C5bB630736C71; address payable public _address1 = payable(_address); // payable address (địa chỉ có thể gửi ETH đi) uint256 public balance = _address1.balance; // số dư của địa chỉ // kiểu bytes (bytes4, bytes8 ... bytes32) (max là 32bytes) bytes32 public _byte32 = "MiniSolidity"; // bytes32: 0x4d696e69536f6c69646974790000000000000000000000000000000000000000 bytes1 public _byte = _byte32[0]; // bytes1: 0x4d // Enum enum ActionSet { Buy, Hold, Sell } ActionSet action = ActionSet.Buy; function enumToUint() external view returns (uint){ return uint(action); }
}
Triển khai trên Remix, ta có thể xem được giá trị của từng biến cũng như kiểu dữ liệu của chúng
3. Function
Một hàm trong Solidity sẽ có dạng tổng quát như sau:
Những phần trong ngoặc vuông [] là tùy chọn
function <function name>(<parameter types>) [internal|external] [pure|view|payable] [returns (<return types>)]
- để định nghĩa 1 hàm thì bắt đầu với từ khóa function
<function name>
là tên của hàm(<parameter types>)
các tham số của hàm (kiểu dữ liệu và tên biến)[internal|external|public|private]
: Mức độ truy cập của hàm
- public: có thể gọi hàm từ bất cứ đâu (giá trị mặc định)
- private: chỉ các hàm trong cùng contract mới gọi được (hàm trong contract kết thừa nó sẽ ko gọi được)
- internal: chỉ có thể gọi từ bên trong contract và các contract kế thừa nó.
- external: Chỉ có thể được gọi từ các contract khác. Nhưng cũng có thể gọi được từ trong contract với từ khoá this
[pure|view|payable]
: payable thêm vào cho các hàm có thể nhận ETH gửi vào. pure và view chúng ta sẽ nói kỹ hơn ở dưới[returns (<return types>)]
: Kiểu dữ liệu trả về, có thể thêm cả tên biến nữa
pure và view
- Giống nhau: Các hàm pure và view đều không mất phí gas khi gọi đến, vì chúng không làm thay đổi trạng thái của blockchain (thay đổi giá trị các biến trong smart contract)
- Khác: Hàm pure không đọc được giá trị các biến trong contract còn view thì có thể
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4; contract FunctionTypes { uint256 public number = 5; // default function add() external { number = number + 1; } // pure (ko lấy được giá trị biến number, chỉ tương tác với tham số truyền vào) function addPure(uint256 _number) external pure returns (uint256 new_number) { new_number = _number + 1; } // view (lấy được giá trị biến number) function addView() external view returns (uint256 new_number) { new_number = number + 1; }
}
4. Return
return và returns
// hàm trả về 3 biến dạng số nguyên dương, boolean và mảng số nguyên dương 3 phần tử function returnMultiple() public pure returns (uint256, bool, uint256[3] memory) { return (1, true, [uint256(1),2,5]); }
returns
: ở cuối định nghĩa của hàm, xác định các kiểu dữ liệu trả về của hàm.return
: câu lệnh ở cuối thân hàm, trả về các giá trị.
Ngoài ra, chúng ta có thể định nghĩa tên các biến sẽ được trả về của hàm, khi đó chúng ta không cần câu lệnh return
ở cuối hàm nữa. Trình biên dịch sẽ tự nhận các biến trả về theo tên
// 3 biến được trả về lần lượt là _number,_bool và _array function returnNamed() public pure returns (uint256 _number, bool _bool, uint256[3] memory _array) { _number = 2; _bool = false; _array = [uint256(3),2,1]; }
Tất nhiên chúng ta vẫn có thể dùng return
nếu thích
function returnNamed2() public pure returns (uint256 _number, bool _bool, uint256[3] memory _array) { return (1, true, [uint256(1),2,5]); }
Destructuring assignments
Đôi khi, đối vói các hàm trả về nhiều giá trị. Chúng ta chỉ cần lấy 1 vài giá trị cần thiết, ko nhất thiết phải lấy tất cả
uint256 _number;
bool _bool;
bool _bool2;
uint256[3] memory _array; // Lấy cả 3 giá trị
(_number, _bool, _array) = returnNamed(); // Chỉ lấy giá trị thứ 2
(, _bool2, ) = returnNamed();
5. Data Storage
Kiểu dữ liệu tham chiếu (Reference types)
Kiểu dữ liệu tham chiếu không lưu trực tiếp giá trị của biến như các kiểu dữ liệu nguyên thủy (uint, bool, int, ...) mà chỉ chứa con trỏ trỏ đến vùng nhớ lưu trữ giá trị đó. Do đó, sẽ phát sinh vấn đề về cấp phát bộ nhớ ... mảng, struct, mapping là các kiểu dữ liệu tham chiếu trong Solidity.
Nơi lưu trữ các biến tham chiếu
Chúng ta có 3 từ khóa storage
, memory
và calldata
. Phí gas sẽ khác nhau đối với từng nơi lưu trữ.
Dữ liệu của một biến storage
được lưu trữ trên blockchain (on-chain), nên sẽ tiêu tốn rất nhiều gas. Trong khi dữ liệu của các biến memory
và calldata được lưu trữ tạm thời trong bộ nhớ, tiêu thụ ít gas hơn.
Các biến memory
và calldata
đều không được lưu trữ on-chain mà chỉ được lưu trữ trên bộ nhớ tạm thời.. Sự khác nhau giữa chúng là biến memory
thay đổi được, còn calldata
thì không.
function fCalldata(uint[] calldata _x) public pure returns (uint[] calldata) { // TypeError: Calldata arrays are read-only _x[0] = 0; return(_x); }
Phạm vi biến
1. Biến trạng thái (State variables)
Các biến trạng thái là các biến có dữ liệu được lưu trữ on-chain, nhưng mức tiêu thụ gas của chúng cao.
Các biến trạng thái được khai báo bên trong hợp đồng và bên ngoài các hàm.
contract Variables { uint public x = 1; uint public y; string public z; function foo() external{ // Chúng ta có thể thay đổi giá trị các biến trạng thái trong hàm x = 5; y = 2; z = "0xAA"; }
}
2. Biến cục bộ (Local variable)
Biến cục bộ là biến chỉ có giá trị trong quá trình thực thi hàm, ra ngoài phạm vi của hàm, chúng sẽ hết hiệu lực. Dữ liệu của các biến cục bộ chỉ được lưu trữ trong bộ nhớ (memory), nên mức tiêu thụ gas sẽ thấp.
Các biến cục bộ được khai báo bên trong một hàm:
function bar() external pure returns (uint){ uint xx = 1; uint yy = 3; uint zz = xx + yy; return(zz); }
3. Biến toàn cục (Global variable )
Biến toàn cục là các biến được Solidity định nghĩa sẵn, chúng ta không cần định nghĩa chúng. Chỉ việc gọi ra để lấy ra giá trị. Danh sách các biến toàn cục của Solidity, các bạn có thể tham khảo tại đây
function global() external view returns (address, uint, bytes memory) { // msg.sender: Địa chỉ gọi đến hàm này address sender = msg.sender; // block.number: Số block mới nhất hiện tại uint blockNum = block.number; // msg.data: data của lời gọi hàm (transaction data bytes memory data = msg.data; return(sender, blockNum, data); }
6. Array, Struct
Mảng (Array)
Có 2 loại mảng trong Solidity là: kích thước cố định (fixed-sized) và mảng động (dynamically-sized arrays).
- Kích thước cố định: Độ dài mảng đã được định nghĩa từ lúc khởi tạo biến.
uint[8] array1; byte[5] array2; address[100] array3;
- Mảng động: Độ dài của mảng không được chỉ định trong quá trình khai báo.
uint[] array4; byte[] array5; address[] array6; bytes array7;
Lưu ý: bytes
là 1 trường hợp đặc biệt, nó là một mảng động, nhưng bạn không cần thêm [] vào sau nó. Chúng ta có thể sử dụng bytes
hoặc bytes1[]
để khai báo mảng bytes. bytes
được khuyến nghị hơn bytes1[]
và cũng tiêu thụ ít gas hơn.
Các hàm với mảng
length
: Trả về số phần tử của mảng.push()
: Thêm phần từ 0 (với string là '') vào cuối mảng (mảng động)push(x)
: Thêm phần tử x vào cuối mảng (mảng động)pop()
: Loại bỏ phần tử ở cuối mảng (mảng động)
contract ArrayTest { uint[] a = [1, 2, 3]; function add() external { a.push(); } function getArr() public view returns (uint[] memory) { return a; }
}
Struct
Chúng ta có thể định nghĩa kiểu dữ liệu mới với struct trong Solidity. Struct cũng là khái niệm đã quen thuộc trong các ngôn ngữ lập trình khác nên chúng ta sẽ không quá khó để làm quen.
contract StructTypes { // Struct struct Student{ uint256 id; uint256 score; } Student student; // Khởi tạo biến student thuộc kiểu Student // Gán giá trị // Cách 1: Tạo 1 biến storage function initStudent1() external{ Student storage _student = student; // _student và student đều trỏ đến vùng nhớ lưu trữ giá trị của biến // Thay đổi _student sẽ thay đổi student luôn _student.id = 11; _student.score = 100; } // Cách 2: thay đổi trực tiếp luôn trên biến student, cách này đỡ cồng kềnh hơn cách 1 function initStudent2() external{ student.id = 1; student.score = 80; } // Cách 3: Gán dùng struct constructor function initStudent3() external { student = Student(3, 90); } // Cách 4: Gán theo kiểu key-value function initStudent4() external { student = Student({id: 4, score: 60}); }
}
7. Mapping
mapping là kiểu dữ liệu dạng key-value trong Solidity. Chúng ta cùng xem 1 vài ví dụ dưới đây nha
mapping (uint => address) public idToAddress; // key dạng uint, value dạng address (idToAddress là tên biến)
mapping (address => address) public swapPair; // mapping từ address này đến address khác (swapPair là tên biến)
Quy tắc của mapping
1. Kiểu dữ liệu của key
Kiểu dữ liệu của key chỉ được là các kiểu dữ liệu nguyên thủy như uint, bool, address ... Kiểu dữ liệu custom như struct sẽ không được chấp nhận.
struct Student{ uint256 id; uint256 score; } // TypeError: Only elementary types, user defined value types, contract types or enums are allowed as mapping keys. mapping(Student => uint) public testVar;
2. Lưu trữ
Mapping phải được lưu trữ on-chain (storage). Nó có thể là biến trạng thái (state variable) hoặc là biến storage
trong thân hàm. Nhưng không thể đóng vai trò làm đối số của hàm hay là giá trị trả về của hàm.
3. Trạng thái public
Nếu mapping được khai báo với từ khóa public thì Solidity sẽ tự động tạo hàm getter để bạn có thể lấy giá trị mapping.
pragma solidity ^0.8.4; contract MappingTest { mapping(address => uint) public testMapping;
}
chúng ta không cần phải nhọc công viết thêm 1 hàm để truy vấn giá trị của mapping nữa vì Solidity đã tạo cho chúng ta hàm testMapping(address)
luôn rồi
4. Thêm cặp key-value mới
Cú pháp thêm cặp key-value vào mapping là _Var[_Key] = _Value
, trong đó _Var
là tên biến mapping, _Key
và_Value
tương ứng với cặp key-value mới.
function writeMap (uint _Key, address _Value) public { idToAddress[_Key] = _Value; }
Một số lưu ý
- Mapping chỉ đơn thuần lưu các cặp key-value, ngoài ra không lưu thêm bất cứ thông tin gì như độ dài key, độ dài value ...
- Mapping là 1 loại bảng băm (hash table) sử dụng thuật toán keccak256
- Các cặp key-value chưa được gán, sẽ trả về giá trị mặc định (0, false, 0x0000000000000000000000000000000000000000 ...)
8. Initial Value
Trong Solidity, các biến được khai báo nhưng chưa được gán giá trị sẽ có giá trị mặc định. Chúng ta hãy cùng xem qua các giá trị mặc định của các kiểu dữ liệu phổ biến.
Các kiểu dữ liệu nguyên thủy
- boolean: false
- string: ""
- int: 0
- uint: 0
- enum: phần tử đầu tiên của enum đó
- address: 0x0000000000000000000000000000000000000000 (address(0))
bool public _bool; // false string public _string; // "" int public _int; // 0 uint public _uint; // 0 address public _address; // 0x0000000000000000000000000000000000000000 enum ActionSet {Buy, Hold, Sell} ActionSet public _enum; // first element 0
Các kiểu dữ liệu tham chiếu
// reference types uint[8] public _staticArray; // [0,0,0,0,0,0,0,0] uint[] public _dynamicArray; // [] mapping(uint => address) public _mapping; // _mapping[a] với a số nguyên dương bất kỳ, ban đầu đều trả về 0x0000000000000000000000000000000000000000 // mặc định tất cả ban đầu là {0, 0} struct Student{ uint256 id; uint256 score; } Student public student;
câu lệnh delete
Sử dụng câu lệnh này sẽ trả biến được thực thi về giá trị mặc định
bool public _bool2 = true; function d() external { delete _bool2; // delete sẽ đưa _bool2 về giá trị false }
9. Constant và Immutable
Hai từ khóa constant và immutable dùng để khai báo những biến hằng, không thể thay đổi giá trị. string
và bytes
có thể khai báo là constant nhưng không thể khai báo với immutable.
Constant
Chúng ta bắt buộc phải gán giá trị khi khai báo constant
uint256 constant CONSTANT_NUM = 10; string constant CONSTANT_STRING = "0xAA"; bytes constant CONSTANT_BYTES = "WTF"; address constant CONSTANT_ADDRESS = 0x0000000000000000000000000000000000000000;
Immutable
Với immutable, chúng ta có thể gán giá trị cho biến ở constructor sau khi khi báo.
uint256 public immutable IMMUTABLE_NUM = 9999999999; address public immutable IMMUTABLE_ADDRESS; uint256 public immutable IMMUTABLE_BLOCK; uint256 public immutable IMMUTABLE_TEST; constructor(){ IMMUTABLE_ADDRESS = address(this); IMMUTABLE_BLOCK = block.number; IMMUTABLE_TEST = test(); } function test() public pure returns(uint256){ uint256 what = 9; return(what); }
10. Control flow
Chúng ta cùng xem qua các câu lệnh điều kiện, lặp trong Solidity
If-else
function ifElseTest(uint256 _number) public pure returns (bool) { if(_number == 0) { return(true); } else { return(false); }
}
For
function forLoopTest() public pure returns (uint256) { uint sum = 0; for(uint i = 0; i < 10; i++) { sum += i; } return(sum);
}
While
function whileTest() public pure returns (uint256) { uint sum = 0; uint i = 0; while(i < 10) { sum += i; i++; } return(sum);
}
Do-while
function doWhileTest() public pure returns (uint256) { uint sum = 0; uint i = 0; do { sum += i; i++; } while (i < 10); return(sum);
}
Toán tử 3 ngôi
function ternaryTest(uint256 x, uint256 y) public pure returns (uint256) { // trả về giá trị lớn hơn giữa x và y return x >= y ? x: y; }
Insert sort trên solidity
function insertionSort(uint[] memory a) public pure returns (uint[] memory) { for (uint i = 1;i < a.length;i++) { uint temp = a[i]; uint j=i; while( (j >= 1) && (temp < a[j-1])) { a[j] = a[j-1]; j--; } a[j] = temp; } return(a); }
11. Constructor & Modifier
Hàm khởi tạo (constructor)
constructor là một hàm sẽ tự động thực thi khi contract được deploy và đó cũng là lần duy nhất nó được gọi.
address owner; // định nghĩa biến owner // constructor constructor() { owner = msg.sender; // gán biến owner bằng địa chỉ deploy contract }
Trước phiên bản solidity 0.4.22, từ khóa constructor
chưa được sử dụng, thay vào đó là tên của smart contract.
pragma solidity = 0.4.21;
contract Parents { function Parents () public { }
}
Modifier
Modifier là 1 chức năng giúp kiểm tra điều kiện thực thi của hàm. Với modifier, chúng ta có thể bỏ bớt các câu điều kiện kiểm tra ở mỗi hàm đi, làm gọn code hơn.
// định nghĩa 1 modifier modifier onlyOwner { require(msg.sender == owner); // kiểm tra xem địa chỉ gọi hàm có phải owner hay không ? _; // execute the function body }
// áp dụng modifier, nếu ko thỏa mãn đk thì sẽ revert ngay function changeOwner(address _newOwner) external onlyOwner { owner = _newOwner; // chỉ owner mới được thay đổi địa chỉ owner }
12. Event
Event trong Solidity là transaction logs của máy ảo EVM. Event sẽ được kích hoạt mỗi khi các hàm chứa nó được gọi. Công dụng của event gồm có:
- Dùng để truy vấn lịch sử giao dịch (gọi qua ethers.js, web3.js ... để duyệt qua các event).
- Tiết kiệm gas, chỉ tốn 2000 gas cho mỗi event thay vì ít nhất 20.000 gas cho các biến storage.
Cú pháp định nghĩa
// event Transfer, có 3 tham số from, to, value.
// Các tham số có indexed sẽ được lưu trữ trong 1 cấu trúc dữ liệu gọi là topics, giúp việc lấy giá trị dễ dàng hơn
event Transfer(address indexed from, address indexed to, uint256 value);
Kích hoạt event (Emit event)
Chúng ta sử dụng từ khóa emit
để kích hoạt event mỗi khi hàm được gọi.
function _transfer( address from, address to, uint256 amount ) external { _balances[from] = 10000000; _balances[from] -= amount; _balances[to] += amount; // emit event emit Transfer(from, to, amount); }
EVM Log
Thông tin về event chúng ta có thể xem ở phần Logs
của mỗi giao dịch (trên etherscan). Logs gồm có 2 phần là topics và data
Topics
Mỗi event sẽ có tối đa 4 topics. Topics[0] là mã băm của tên event có dạng như sau:
//0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
keccak256("Transfer(addrses,address,uint256)")
Như đã đề cập ở phần trên, các tham số có đi kèm từ khóa indexed
sẽ được lưu vào topics. Suy ra sẽ có tối đa 3 tham số indexed
trong event.
Kích thước mỗi phần tử trong cấu trúc dữ liệu topics là 32 bytes, với các kiểu dữ liệu nhiều hơn 32 bytes như mảng hay chuỗi thì nó sẽ được lưu trữ dưới dạng băm
Data
Các tham số không được định nghĩa với indexed
sẽ được lưu trong Data. Data không giới hạn kích thước lưu trữ nên có thể lưu các cấu trúc dữ liệu phức tạp và nó cũng tiết kiệm gas hơn là lưu ở topics
Remix demo
Chúng ta có đoạn code sau:
// SPDX-License-Identifier: Unlicense pragma solidity ^0.8.19; contract EventTest { mapping (address => uint256) public balance; // Định nghĩa event event Transfer(address indexed from, address indexed to, uint256 value); function transfer(address from, address to, uint256 amount) external { balance[from] = 10000000; balance[from] -= amount; balance[to] += amount; // Kích hoạt event emit Transfer(from, to, amount); }
}
Deploy và gọi hàm transfer, chúng ta sẽ thấy event được kích hoạt
13. Kế thừa (Inheritance)
Kế thừa là một khái niệm rất quen thuộc trong lập trình hướng đối tượng (object-oriented programming). Lớp con sẽ có các thuộc tính từ lớp cha cũng như có thể dùng lại các biến, hàm đã được được nghĩa từ lớp mà nó kế thừa (trừ biến private).
Trong Solidity, mỗi contract tương đương với một lớp trong khái niệm lập trình hướng đối tượng.
Hai từ khóa quan trọng để nắm được kế thừa trong Solidity:
virtual
: Các hàm ở contract cha dự kiến sẽ được ghi đè trong các contract con sẽ được định nghĩa kèm từ khóa virtual.override
: Các hàm được ghi đè ở contract con sẽ được định nghĩa kèm từ khóa override.
**Lưu ý 1 **: Nếu 1 hàm được ghi đè và dự kiến sẽ ghi đè tiếp ở các contract con thì sẽ định nghĩa với virtual override
.
Lưu ý 2:
Với các biến public, nếu dùng với từ khóa override
thì hàm getter sẽ được ghi đè.
mapping(address => uint256) public override balanceOf;
`` ## Đơn kế thừa Ta có contract `Granfather`, contract `Father` sẽ kế thừa `Grandfather` và ghi đè cả 2 hàm hip, pop, ```js
contract Grandfather { event Log(string msg); function hip() public virtual{ emit Log("Grandfather"); } function pop() public virtual{ emit Log("Grandfather"); }
} contract Father is Grandfather{ function hip() public virtual override{ emit Log("Father"); } function pop() public virtual override{ emit Log("Father"); }
}
Đa kề thừa (Multiple inheritance)
Son
kế thừa cả Father
lẫn Grandfather
contract Son is Grandfather, Father { function hip() public virtual override(Grandfather, Father){ emit Log("Son"); } function pop() public virtual override(Grandfather, Father) { emit Log("Son"); }
- Với đa kế thừa, chúng ta phải sắp xếp theo thứ tự từ cao đến thấp. Ví dụ như
contract Son is Grandfather, Father
thay vìcontract Son is Father, Grandfather
. - Nếu 1 hàm tồn tại trong nhiều contract cha thì bắt buộc phải ghi đè ở contract con. Ví dụ như contract
Son
bắt buộc ghi đè cả 2 hàm, nếu không sẽ lỗi. - Khi hàm ghi đè tồn tại ở nhiều contract cha thì cần định nghĩa thêm với từ khóa
override
. Ví như nhưoverride(Grandfather, Father)
Kế thừa modifiers
Kế thừa modifiers cần thêm từ khóa virtual
.
contract Base1 { modifier exactDividedBy2And3(uint _a) virtual { require(_a % 2 == 0 && _a % 3 == 0); _; }
} contract Identifier is Base1 { function getExactDividedBy2And3(uint _dividend) public exactDividedBy2And3(_dividend) pure returns(uint, uint) { return getExactDividedBy2And3WithoutModifier(_dividend); } function getExactDividedBy2And3WithoutModifier(uint _dividend) public pure returns(uint, uint){ uint div2 = _dividend / 2; uint div3 = _dividend / 3; return (div2, div3); }
}
Chúng ta cũng có thể ghi đè modifiers nếu cần thiết
modifier exactDividedBy2And3(uint _a) override { _; require(_a % 2 == 0 && _a % 3 == 0); }
Kế thừa hàm khởi tạo (constructors)
abstract contract A { uint public a; constructor(uint _a) { a = _a; }
}
Có 2 cách để kế thừa hàm khởi tạo trong Solidity
contract B is A(1) { }
hoặc
contract C is A { constructor(uint _c) A(_c * _c) {}
}
Gọi hàm từ contract cha
Chúng ta cũng có hai cách
// Gọi trực tiếp bằng tên contract function callParent() public{ Grandfather.pop(); }
function callParentSuper() public{ // dùng từ khóa super, trong TH này sẽ gọi contract cha gần nó nhất. Vd như Son sẽ gọi Father.pop() super.pop(); }
Kế thừa kiểu kim cương (Diamond inherit)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13; /* Inheritance tree visualized: God / \
Adam Eve \ /
people
*/
contract God { event Log(string message); function foo() public virtual { emit Log("God.foo called"); } function bar() public virtual { emit Log("God.bar called"); }
}
contract Adam is God { function foo() public virtual override { emit Log("Adam.foo called"); Adam.foo(); } function bar() public virtual override { emit Log("Adam.bar called"); super.bar(); }
}
contract Eve is God { function foo() public virtual override { emit Log("Eve.foo called"); Eve.foo(); } function bar() public virtual override { emit Log("Eve.bar called"); super.bar(); }
}
contract People is Adam, Eve { function foo() public override(Adam, Eve) { super.foo(); } function bar() public override(Adam, Eve) { super.bar(); }
}
Adam
và Eve
là lớp cha ngay trên của People
. Vậy khi gọi hàm bar()
trên People
, các hàm nào sẽ được gọi :-? Chúng ta cùng thử chạy và xem log như thế nào ?
Vậy là hàm bar
trong cả 2 contract Adam
và Eve
đều được gọi, cả hàm bar
trong God
nữa. Hàm bar của God chỉ được gọi 1 lần mặc dù, Adam và Eve đều gọi đến. Đây là thiết kế của Solidity tham khảo từ Python dựa trên mô hình DAG (directed acyclic graph), Chúng ta có thể tham khảo chi tiết hơn ở đây
14. Abstract and Interface
Abstract contract
Contract nào có ít nhất 1 hàm được định nghĩa nhưng không có thân hàm (unimplemented function) thì được gọi là abstract contract.
abstract contract InsertionSort { // unimplemented function cần phải có từ khóa virtual function insertionSort(uint[] memory a) public pure virtual returns(uint[] memory);
}
Interface
Interface cũng gần tương tự abstract contract, nhưng sẽ có thêm nhiều quy tắc như sau:
- Không có biến trạng thái
- Không có hàm khởi tạo
- Không thể kế thừa các hợp đồng thông thường (non-interface contracts)
- Tất cả các hàm định nghĩa là
external
và không được có thân hàm. - Contract nào kế thừa interface thì phải triển khai logic cho tất cả các hàm đã được định nghĩa trong interface.
interface IERC721 is IERC165 { event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId); event ApprovalForAll(address indexed owner, address indexed operator, bool approved); function balanceOf(address owner) external view returns (uint256 balance); function ownerOf(uint256 tokenId) external view returns (address owner); function safeTransferFrom(address from, address to, uint256 tokenId) external; function transferFrom(address from, address to, uint256 tokenId) external; function approve(address to, uint256 tokenId) external; function getApproved(uint256 tokenId) external view returns (address operator); function setApprovalForAll(address operator, bool _approved) external; function isApprovedForAll(address owner, address operator) external view returns (bool); function safeTransferFrom( address from, address to, uint256 tokenId, bytes calldata data) external;
}
Khi ta biết 1 contract kế thừa interface nào đó, chúng ta có thể sử dụng interface đó để tương tác với contract cần gọi.
contract interactBAYC { // Gọi đến contract ERC721 qua interface IERC721 IERC721 BAYC = IERC721(0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D); function balanceOfBAYC(address owner) external view returns (uint256 balance){ return BAYC.balanceOf(owner); } function safeTransferFromBAYC(address from, address to, uint256 tokenId) external{ BAYC.safeTransferFrom(from, to, tokenId); }
}
14. Errors
error
error là khái niệm mới được giới thiệu từ phiên bản Solidity 0.8
. Tiết kiệm gas và có thể thông báo tên lỗi cho phía người dùng.
error TransferNotOwner(); // định nghĩa tên lỗi function transferOwner1(uint256 tokenId, address newOwner) public { if(_owners[tokenId] != msg.sender){ // throw lỗi revert TransferNotOwner(); } _owners[tokenId] = newOwner;
}
require
Là cách kiểm tra điều kiện đầu vào và revert nếu điều kiện không thỏa mãn. require
tốn nhiều gas hơn error
và lượng gas tăng theo độ dài của thông báo lỗi. Nhưng với sự quen thuộc với nhiều lập trình viên, nó vẫn được sử dụng.
function transferOwner2(uint256 tokenId, address newOwner) public { require(_owners[tokenId] == msg.sender, "Transfer Not Owner"); _owners[tokenId] = newOwner;
}
assert
Câu lệnh assert
được dùng nhiều để gỡ lỗi (debug) vì nó không revert và cũng ko có thông báo lỗi khi kiểm tra điều kiện.
function transferOwner3(uint256 tokenId, address newOwner) public { assert(_owners[tokenId] == msg.sender); _owners[tokenId] = newOwner; }