+1

WTF Solidity 101

image.png

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

image.png

2. Code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract HelloWeb3 {
    string public hello = "Hello Web3!";
}
    1. 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.
    1. 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 ;
    1. 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.
    1. 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

image.png

  • Ở phần Deployed Contracts ta sẽ tương tác được với smart contract vừa deploy

image.png

  • Ấ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

  1. 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.
  2. Kiểu tham chiếu (Reference Type) : Gồm mảng và struct
  3. Ánh xạ (Mapping Type) : Mapping
  4. 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

image.png

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>)]
  1. để định nghĩa 1 hàm thì bắt đầu với từ khóa function
  2. <function name> là tên của hàm
  3. (<parameter types>) các tham số của hàm (kiểu dữ liệu và tên biến)
  4. [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
  1. [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
  2. [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 pureview đề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ể

image.png

// 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, memorycalldata. 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 memorycalldata đề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);
    }

image.png

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;
    }
}

image.png

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;
}

image.png

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_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 ý

  1. 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 ...
  2. Mapping là 1 loại bảng băm (hash table) sử dụng thuật toán keccak256
  3. 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 constantimmutable dùng để khai báo những biến hằng, không thể thay đổi giá trị. stringbytes 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à topicsdata

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

image.png

image.png

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();
    }
}

AdamEve 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 ?

image.png

Vậy là hàm bar trong cả 2 contract AdamEve đề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:

  1. Không có biến trạng thái
  2. Không có hàm khởi tạo
  3. Không thể kế thừa các hợp đồng thông thường (non-interface contracts)
  4. Tất cả các hàm định nghĩa là external và không được có thân hàm.
  5. 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;
    }

Nguồn, tài liệu tham khảo

https://github.com/AmazingAng/WTF-Solidity

https://www.wtf.academy/en/solidity-start/


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí