
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ọnif (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ênKế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
**balance()**
: Biểu diễn số Ether hiện có trong tài khoản địa chỉ đó.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.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.**call()**
: Phương thức được sử dụng để gọi một hàm (function) trên một địa chỉ khác.**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.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.gas
: Biểu diễn số Gas (đơn vị phí của Ethereum) còn lại trong tài khoản địa chỉ đó.**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 khiblocknumber
là một trong 256 khối gần đây nhất; mặt khác trả về số khôngblock.basefee
(uint
): phí cơ bản của khối hiện tạiblock.chainid
(uint
): id chuỗi hiện tạiblock.coinbase
( ): địa chỉ của công cụ khai thác khối hiện tạiaddress payable
block.difficulty
(uint
): độ 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.gaslimit
(uint
): gaslimit khối hiện tạiblock.number
(uint
): số khối hiện tạiblock.prevrandao
(uint
): số ngẫu nhiên được cung cấp bởi chuỗi đèn hiệu ( )EVM >= Paris
block.timestamp
(uint
): dấu thời gian của khối hiện tại tính bằng giây kể từ kỷ nguyên unixgasleft() returns (uint256)
: khí còn lạimsg.data
( ): hoàn thành calldatabytes calldata
msg.sender
(address
): người gửi tin nhắn (cuộc gọi hiện tại)msg.sig
(bytes4
): bốn byte đầu tiên của calldata (tức là định danh hàm)msg.value
(uint
): số wei được gửi cùng với tin nhắntx.gasprice
(uint
): giá gas của giao dịchtx.origin
(address
): 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ố đã choabi.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 đã choabi.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 đếnfunctionPointer
vớ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áirevert(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àosha256(bytes memory) returns (bytes32)
tính toán hàm băm SHA-256 của đầu vàoripemd160(bytes memory) returns (bytes20)
tính toán hàm băm RIPEMD-160 của đầu vàoecrecover(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).name
Tên của hợp đồng.type(C).creationCode
Bộ 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).runtimeCode
Mả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).min
Giá trị nhỏ nhất có thể đại diện bởi loạiT
.type(T).max
Giá trị lớn nhất có thể đại diện bởi loạiT
.
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ó: if
, else
, while
, do
, for
, break
, continue
, return
Solidity cũng hỗ trợ xử lý ngoại lệ dưới dạng câu lệnh try
/ catch
, 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 đượcTrì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 externalf
không thể được gọi bên trong (nghĩa làf()
không hoạt động, nhưngthis.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.