logo
banner
avatar
Nhabachoc Dũng
Thứ năm 2023
Dev nào mua Solidity cơ bản đêeeeeeeeee 😆
# javascript
# html
# blockchain

Chao xìn mọi người lại là mình đây Nhabachoc KAD.

Bài viết này có dành cho bạn?

  • Đọc về Solidity trên mạng nhưng cảm thấy chưa đầy đủ

  • Muốn có một cái nhìn tổng quan nhất về Solidity

Hành trang cần chuẩn bị:

  • Kiến thức cơ bản về lập trình nói chung

Cùng tắt đèn bật ý tưởng, gét go 🪄

Bài 1: Tổng quan

Solidity là một ngôn ngữ lập trình dành cho các hợp đồng thông minh (smart contracts) trên mạng blockchain Ethereum, ra mắt lần đầu vào năm 2014. Nó được tạo ra để giúp các lập trình viên xây dựng và triển khai các hợp đồng thông minh một cách dễ dàng và mạnh mẽ. Có 3 đặc điểm chính cần biết trước khi chúng ta bắt tay vào “giã con hàng” solidity:

  • Biên dịch<compile>: mã nguồn của chương trình được biên dịch sang mã máy trước khi chạy

  • Kiểu tĩnh<statically typed>: định danh kiểu dữ liệu trước khi dùng

  • Hướng đối tượng<object oriented>: tập trung mô hình hoá đối tượng

Đang phổ biến nhất trên Internet khi nói về Solidity là các ứng dụng như:

  • Ứng dụng Phi tập trung <DAPPs>

  • Ứng dụng Tài chính <DEFI>

  • Ứng dụng Tài sản điện tử <NFT>

Khi nói đến Solidity thì ta đang nói đến 1 chương trình - 1 quy tắc luật lệ trên blockchain. Quy tắc và luật lệ 1 khi được đẩy lên sẽ vĩnh viễn ở đấy và không thay đổi đó chính là tiền đề thay thế những khái niệm người trung gian.


Bài 2: Các thành phần cơ bản

Có 4 thành phần mà trong mọi dự án cần có:

  • Pragma

  • State

  • Function

  • Modifier

  • Event

  • Struct

  • Enum

1. Pragma

Trong Solidity pragma là một từ khóa đặc biệt dùng để chỉ ra Phiên bản của ngôn ngữ mà hợp đồng thông minh đang sử dụng.

pragma solidity 0.7.0; // bắt buộc phải là phiên bản 0.7.0

pragma solidity ^0.7.0;
// có thể sử dụng các phiên bản lớn hơn nhưng phải tương thích ngược
// nhận các phiên bản 0.7.0 -> 0.7.9 

2. State

pragma solidity ^0.7.0;

contract Cat {
	uint name; // Biến State
	// {kiểu dữ liệu} {tên biến}

3. Function

pragma solidity ^0.7.0;

contract Cat {
	funtion sound() public return (uint) {
		//...
	}

4. Modifier

pragma solidity ^0.7.0;

contract Cat {
	modifier onlyCat() **{
		require(msg.sender == cat, “Day khong phai meo");
		_;
	}**

	funtion sound() public view onlyCat {
		//...
	}

5. Event

pragma solidity ^0.7.0;

contract Cat {
	event Meow(address cat, string sound);
	
	funtion sound() public {
		//...
		emit Meow(cat, 'Meowwww!')
	}

6. Error

pragma solidity ^0.7.0;

error NotCat(string error);

contract Cat {
	event Meow(address cat, string sound);
	
	funtion sound() public {
		//...
		revert NotCat('Gau Gauuu!')
	}

7. Struct

Tạo ra kiểu dữ liệu mới theo ý người lập trình viên

pragma solidity ^0.7.0;

struct Cat {
	string name;
	uint old;
	string color;
}

8. Enum

pragma solidity ^0.7.0;

enum Animal {
	Cat,
	Dog,
	Chicken
}

Animal public animal;
animal = Animal.Cat

Bài 3: Kiểu dữ liệu cơ bản

1. Boolean

Lưu trữ

1 bit giá trị: true hoặc false

Các phép toán

!&&||==!=

Lưu ý:

  • ||, && sử dụng quy tắc rút gọn if (x > 0 || y > 0) { ... } nếu x là dương điều kiện đúng mà không cần kiểm tra giá trị y

2. Interger

Lưu trữ

8 đến 256 bit giá trị theo bước 8:

  • Số nguyên âm, dương: int8, int16, int32, int64, int128, int256

  • Số nguyên dương: uint8, uint16, uint32, uint64, uint128, uint256

int/uint là viết tắt của int256/uint256

Lưu ý:

  • Với các số nguyên dương <không quan tâm đến dấu vì luôn là dấu cộng> thì giá trị tối đa của biến là 2^n-1 với n là số bit, VD: uint8 có giá trị tối đa là 2^8-1= 255.

  • Với các số nguyên có dấu thì bit đầu tiên được dùng để đại diện cho dấu của số. Bit 0 ⇒ dương, Bit 1 ⇒ âm

Các phép toán

  • So sánh <=<==!=>=>

  • Phép toán bit & (and)| (or)^ (bitwise exclusive)~ (bitwise negation)

  • Toán tử dịch chuyển: <<(chuyển trái), >>(chuyển phải)

  • Toán tử số học + (cộng), - (trừ)* (nhân)/ (chia)% (lấy dư)** (lữu thừa)

Lưu ý:

  • Đối với kiểu giữ liệu số nguyên X, bạn có thể sử dụng type(X).min và type(X).max để kiểm tra giá trị min max.

  • Mặc định chương trình luôn kiểm tra: nếu thao tác tính toán nằm ngoài phạm vi giá trị của biến thì cuộc gọi <khi bạn mua bán …> thì sẽ bị hoàn nguyên thông báo không thành công

    unchecked { return a - b; }
    
    //chia cho 0 lỗi Panic kể cả dùng unchecked
    
  • int256(-5) / int256(2) == int256(-2) phép chia luôn làm tròn để kết quả là số nguyên

  • Kết quả phép modulo cùng dấu với toạn hạng đầu tiên

  • 0**0 = 1'

3. Fixed Point Numbers

Lưu trữ

Các kiểu trên thì khá quen thuộc con đây là kiểu lưu trữ số thực.

Solidity hỗ trợ 2 loại:

  • fixed : biểu diễn số thực có dấu

  • unfixed : biểu diễn số thực ko dấu

Đươc định nghĩa bằng cách chỉ định số bit dành cho phần nguyên và phần thập phân. VD: fixed16x8 myNumber - với độ chính xác 16 bit và 8 bit cho phần thập phân.

Các phép toán

  • So sánh <=<==!=>=>

  • Toán tử số học + (cộng),- (trừ)* (nhân)/ (chia)% (lấy dư)

4. Address

Lưu trữ

giá trị 20byte thể hiện dãy địa chỉ tài khoản Ethereum

Có 2 các khai báo:

  • Address: khai báo địa chỉ thông thường

  • Address payable: là địa chỉ bạn có thể gửi ETH, bao gồm 2 phương thức:

    • Transfer

    • Send

Các phép toán

  • So sánh: <=<==!=>=, >

Lưu ý:

Nếu bạn chuyển đổi một giá trị có kích thước 32 byte thành một địa chỉ Ethereum, các byte đầu tiên sẽ bị cắt bớt để phù hợp với kích thước 20 byte của kiểu address theo 2 cách:

  • address(uint160(bytes20(b)))

  • address(uint160(uint256(b)))

Các phương thức

  1. **balance()**: Biểu diễn số Ether hiện có trong tài khoản địa chỉ đó.

  2. transfer(): Phương thức được sử dụng để chuyển tiền (Ether) từ địa chỉ hiện tại sang địa chỉ khác. Phương thức này trả về một giá trị kiểu bool, xác định liệu việc chuyển tiền có thành công hay không.

  3. send(): Phương thức khác được sử dụng để chuyển tiền (Ether) từ địa chỉ hiện tại sang địa chỉ khác, tuy nhiên phương thức này trả về một giá trị kiểu bool và sẽ gửi một sự kiện (event) nếu việc chuyển tiền không thành công.

  4. **call()**: Phương thức được sử dụng để gọi một hàm (function) trên một địa chỉ khác.

  5. **delegatecall()**: Phương thức tương tự như call(), tuy nhiên nó chỉ thay đổi các biến trên địa chỉ hiện tại thay vì trên địa chỉ khác.

  6. staticcall(): Phương thức gọi hàm trên một địa chỉ khác, nhưng không thay đổi bất kỳ trạng thái nào trên blockchain. Nó được sử dụng để thực hiện các tác vụ chỉ đọc dữ liệu từ một hợp đồng khác hoặc kiểm tra trạng thái của một hợp đồng khác mà không cần tốn phí gas.

  7. gas: Biểu diễn số Gas (đơn vị phí của Ethereum) còn lại trong tài khoản địa chỉ đó.

  8. **codehash**: Biểu diễn mã hash của mã bytecode của hợp đồng tại địa chỉ đó.


Bài 4: Các kiểu dữ liệu liên quan

1. Vị trị lưu dữ liệu

  • Memory: là vùng lưu trữ tạm thời trong thời gian thực thi hàm. Khi một hàm được gọi, các biến được khai báo với từ khóa "memory" sẽ được lưu trữ trong bộ nhớ tạm thời này. Thông thường, các biến này được sử dụng để lưu trữ các giá trị tạm thời hoặc để truyền dữ liệu giữa các hàm.

  • Storage: là vùng lưu trữ vĩnh viễn trên blockchain. Các biến được khai báo với từ khóa "storage" sẽ được lưu trữ vĩnh viễn trên blockchain. Thông thường, các biến này được sử dụng để lưu trữ trạng thái của smart contract.

  • Calldata: là vùng lưu trữ cho các tham số đầu vào của một hàm. Khi một hàm được gọi, các tham số được truyền vào sẽ được lưu trữ trong vùng calldata này. Vùng calldata là chỉ đọc và không thể được sửa đổi, do đó nó được sử dụng để truyền các tham số đầu vào của hàm mà không ảnh hưởng đến trạng thái của smart contract.

2. Array

Tập các giá trị có cùng kiểu, đặc biệt nó có thể có kích thước cố định hoặc động:

pragma solidity ^0.7.0;

uint[] cats; // mảng con mèo có kích thước động
uint[3] cats = [1, 2, 3]; // mảng con mèo có kích thước cố định là 3
uint[] cats = [1, 2, 3];
// khai báo [] nhưng gán 3 phần tử nên mảng con mèo có kích thước cố định là 3

Mapping

Cặp giá trị được ghép cùng nhau

pragma solidity ^0.7.0;

struct Cat {
	string name;
	uint old;
	string color;
}

mapping(uint => Cat) public listCats

Bài 5: Biến Global

Ether

assert(1 wei == 1);
assert(1 gwei == 1e9);
assert(1 ether == 1e18);

Thời gian

  • 1 == 1 seconds

  • 1 minutes == 60 seconds

  • 1 hours == 60 minutes

  • 1 days == 24 hours

  • 1 weeks == 7 days

Biến và hàm

Thuộc tính trong khối và giao dịch:

  • blockhash(uint blockNumber) returns (bytes32): hàm băm của khối đã cho khi blocknumberlà một trong 256 khối gần đây nhất; mặt khác trả về số không

  • block.basefeeuint): phí cơ bản của khối hiện tại

  • block.chainiduint): id chuỗi hiện tại

  • block.coinbase( ): địa chỉ của công cụ khai thác khối hiện tạiaddress payable

  • block.difficultyuint): độ khó khối hiện tại ( ). Đối với các phiên bản EVM khác, nó hoạt động như một bí danh không dùng nữa choEVM < Parisblock.prevrandao

  • block.gaslimituint): gaslimit khối hiện tại

  • block.numberuint): số khối hiện tại

  • block.prevrandaouint): số ngẫu nhiên được cung cấp bởi chuỗi đèn hiệu ( )EVM >= Paris

  • block.timestampuint): dấu thời gian của khối hiện tại tính bằng giây kể từ kỷ nguyên unix

  • gasleft() returns (uint256): khí còn lại

  • msg.data( ): hoàn thành calldatabytes calldata

  • msg.senderaddress): người gửi tin nhắn (cuộc gọi hiện tại)

  • msg.sigbytes4): bốn byte đầu tiên của calldata (tức là định danh hàm)

  • msg.valueuint): số wei được gửi cùng với tin nhắn

  • tx.gaspriceuint): giá gas của giao dịch

  • tx.originaddress): người gửi giao dịch (chuỗi cuộc gọi đầy đủ)

Mã hoá, giải mã ABI:

  • abi.decode(bytes memory encodedData, (...)) returns (...): ABI-giải mã dữ liệu đã cho, trong khi các loại được đưa ra trong ngoặc đơn làm đối số thứ hai. Ví dụ:(uint a, uint[2] memory b, bytes memory c) = abi.decode(data, (uint, uint[2], bytes))

  • abi.encode(...) returns (bytes memory): ABI-mã hóa các đối số đã cho

  • abi.encodePacked(...) returns (bytes memory): Thực hiện mã hóa đóng gói của các đối số đã cho. Lưu ý rằng mã hóa đóng gói có thể không rõ ràng!

  • abi.encodeWithSelector(bytes4 selector, ...) returns (bytes memory): ABI mã hóa các đối số đã cho bắt đầu từ đối số thứ hai và thêm vào trước bộ chọn bốn byte đã cho

  • abi.encodeWithSignature(string memory signature, ...) returns (bytes memory): Tương đương vớiTương đươngĐẾNabi.encodeWithSelector(bytes4(keccak256(bytes(signature))), ...)

  • abi.encodeCall(function functionPointer, (...)) returns (bytes memory): ABI-mã hóa một cuộc gọi đến functionPointervới các đối số được tìm thấy trong bộ dữ liệu. Thực hiện kiểm tra loại đầy đủ, đảm bảo các loại khớp với chữ ký chức năng. Kết quả bằngabi.encodeWithSelector(functionPointer.selector, (...))

Xử lý lỗi:

  • assert(bool condition): Gây ra lỗi Hoảng loạn và do đó, trạng thái thay đổi hoàn nguyên nếu điều kiện không được đáp ứng - được sử dụng cho các lỗi nội bộ.

  • require(bool condition): Hoàn nguyên nếu điều kiện không được đáp ứng - được sử dụng cho các lỗi trong đầu vào hoặc các thành phần bên ngoài.

  • require(bool condition, string memory message): Hoàn nguyên nếu điều kiện không được đáp ứng - được sử dụng cho các lỗi trong đầu vào hoặc các thành phần bên ngoài. Cũng cung cấp một thông báo lỗi.

  • revert(): Hủy bỏ thực thi và hoàn nguyên các thay đổi trạng thái

  • revert(string memory reason): Hủy bỏ thực thi và hoàn nguyên các thay đổi trạng thái, cung cấp một chuỗi giải thích

Hàm toán học và mật mã

  • addmod(uint x, uint y, uint k) returns (uint)tính toán trong đó phép cộng được thực hiện với độ chính xác tùy ý và không bao quanh tại . Khẳng định rằng bắt đầu từ phiên bản 0.5.0.(x + y) % k2**256k != 0

  • mulmod(uint x, uint y, uint k) returns (uint)tính toán trong đó phép nhân được thực hiện với độ chính xác tùy ý và không bao quanh tại . Khẳng định rằng bắt đầu từ phiên bản 0.5.0.(x * y) % k2**256k != 0

  • keccak256(bytes memory) returns (bytes32)tính toán hàm băm Keccak-256 của đầu vào

  • sha256(bytes memory) returns (bytes32)tính toán hàm băm SHA-256 của đầu vào

  • ripemd160(bytes memory) returns (bytes20)tính toán hàm băm RIPEMD-160 của đầu vào

  • ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) returns (address)khôi phục địa chỉ được liên kết với khóa chung từ chữ ký đường cong elip hoặc trả về 0 do lỗi. Các tham số chức năng tương ứng với các giá trị ECDSA của chữ ký: • r= 32 byte chữ ký đầu tiên • s= 32 byte thứ hai của chữ ký • v= 1 byte chữ ký cuối cùng

Các phương thức của Address

Type

type(X) đang hỗ trợ cho số nguyên và địa chỉ gồm các phương thức:

  • type(C).nameTên của hợp đồng.

  • type(C).creationCodeBộ nhớ chứa bytecode của hợp đồng. Thuộc tính này không thể được truy cập trong chính hợp đồng hoặc bất kỳ hợp đồng phái sinh nào.

  • type(C).runtimeCodeMảng byte bộ nhớ chứa bytecode đang chạy của hợp đồng.

Ngoài các thuộc tính ở trên, các thuộc tính sau có sẵn cho một loại giao diện I:

  • type(I).interfaceId: Giá trị bytes4 của interface trong Solidity có một interfaceId duy nhất, được tính toán từ tên interface và các hàm của nó, bằng cách sử dụng thuật toán keccak256.

Các thuộc tính sau đây có sẵn cho một kiểu số nguyên T:

  • type(T).minGiá trị nhỏ nhất có thể đại diện bởi loại T.

  • type(T).maxGiá trị lớn nhất có thể đại diện bởi loại T.


Bài 6: Biểu thức và cấu trúc điều khiển

Cấu trúc điều kiện

Có: ifelsewhiledoforbreakcontinuereturn

Solidity cũng hỗ trợ xử lý ngoại lệ dưới dạng câu lệnh trycatch, nhưng chỉ dành cho các lệnh gọi hàm bên ngoài và lệnh gọi tạo hợp đồng. Lỗi có thể được tạo bằng cách sử dụng câu lệnh revert.

Gọi hàm

Nội bộ

Các lời gọi hàm nội bộ được biên dịch thành JUMP bên trong EVM ⇒ có tác dụng là bộ nhớ hiện tại không bị xóa, nghĩa là chuyển các tham chiếu bộ nhớ tới các hàm được gọi nội bộ là rất hiệu quả. Chỉ các hàm của cùng một hợp đồng mới có thể được gọi nội bộ.

Bạn vẫn nên tránh đệ quy quá mức, vì mọi lệnh gọi hàm nội bộ đều sử dụng ít nhất 1 slot trong stack và chỉ có 1024 slot khả dụng.

Bên ngoài

Đối với một cuộc gọi bên ngoài, tất cả các đối số chức năng phải được sao chép vào bộ nhớ.

Do EVM coi lệnh gọi đến hợp đồng không tồn tại luôn thành công, Solidity sử dụng opcode extcodesizeđể kiểm tra xem hợp đồng sắp được gọi có thực sự tồn tại hay không (có chứa mã) và gây ra ngoại lệ nếu không.

Tạo hợp đồng

New

Full code của hợp đồng đang được tạo phải được biết khi biên dịch để không thể thực hiện được các phụ thuộc tạo đệ quy.

Create2


Bài 7: Contract

Khi một hợp đồng được tạo, hàm khởi tạo của nó (constructor) sẽ được thực thi một lần. Một hàm khởi tạo là tùy chọn không bắt buộc và chỉ cho phép một hàm khởi tạo.

Khả năng truy cập

Biến

  • public: Ai truy cập cũng được

    Trình biên dịch tự động tạo các hàm getter cho tất cả các biến public. VD:

    contract C {
        uint public data = 42;
    }
    
    contract Caller {
        C c = new C();
        function f() public view returns (uint) {
            return c.data();
        }
    }
    

    Không có hàm setter

  • internal - Default: Chỉ có thể truy cập bên trong hợp đồng hoặc từ hợp đồng con.

  • private: Giống như internal nhưng không thể truy cập từ hợp đồng con.

Hàm

  • external: Có thể gọi từ các hợp đồng khác. Function externalfkhông thể được gọi bên trong (nghĩa là f()không hoạt động, nhưng this.f()hoạt động).

  • public: Có thể gọi nội bộ hoặc thông qua các cuộc gọi tin nhắn.

  • internal: Chỉ có thể truy cập từ bên trong hợp đồng hiện tại hoặc các hợp đồng con. Không thể được truy cập từ bên ngoài vì chúng không được hiển thị ra bên ngoài thông qua ABI của hợp đồng.

  • private: Giống như internal nhưng không thể truy cập từ hợp đồng con.

Đọc tiếp
Cách tạo ra một database lưu trữ dữ liệu từ đầu
Khổng Anh Dũng - Thứ5 15
Cách tạo ra một database lưu trữ dữ liệu từ đầu
Khổng Anh Dũng - Thứ5 15
Cách tạo ra một database lưu trữ dữ liệu từ đầu
Khổng Anh Dũng - Thứ5 15