Học cách phát triển Smart Contract Solidity bằng thư viện Foundry
Chào mừng bạn đến với repository của khoá học Foundry Basics.Khoá học này được phát triển bởi VBI Academy và Terran Crypt.
Nội dung trong khoá học này đã được sự cho phép chọn lọc và dịch thuật từ các khoá học được phát triển và giảng dạy bởi Cyfrin Updraft và Patrick Collins.
Tiếp nối khoá học Solidity Basics, trong khoá học này chúng ta sẽ học cách sử dụng Foundry cho việc xây dựng smart contract Solidity. Hãy chắc chắn rằng bạn đã có kiến thức cơ bản về Blockchain và Solidity trước khi chúng ta học khoá này.
Group hỗ trợ: Solidity Developer Vietnam
- Mở đầu
- Section 1: Local Development w/ Foundry
- Section 2: Smart Contracts Testing w/ Foundry
- Coverage (độ bao phủ test)
- Section 3: Full Stack Web3 Developer with Decentralized Crowdfunding's Interface
- Section 4: Lottery Contract
Foundry là một bộ công cụ phát triển smart contract cho Ethereum, được viết bằng Rust. Nó được thiết kế để hỗ trợ việc phát triển, kiểm thử và triển khai smart contract Solidity một cách hiệu quả.
-
Các thành phần chính:
- Forge: Công cụ kiểm thử và biên dịch smart contract
- Cast: Công cụ dòng lệnh để tương tác với blockchain Ethereum
- Anvil: Node Ethereum cục bộ để phát triển và kiểm thử
-
Ưu điểm chính:
- Tốc độ: Được viết bằng Rust, Foundry thực hiện các tác vụ như biên dịch và kiểm thử nhanh hơn so với nhiều công cụ khác.
- Kiểm thử mạnh mẽ: Hỗ trợ viết và chạy các bài kiểm tra bằng Solidity, cho phép kiểm tra kỹ lưỡng logic của smart contract.
- Tích hợp với các công cụ khác: Dễ dàng tích hợp với các công cụ phát triển phổ biến khác.
- Debugger tích hợp: Giúp dễ dàng tìm và sửa lỗi trong smart contract.
-
Cách sử dụng:
- Viết smart contract bằng Solidity
- Sử dụng Forge để biên dịch và kiểm thử contract
- Sử dụng Cast để tương tác với blockchain
- Sử dụng Anvil để chạy một node Ethereum cục bộ cho phát triển
Code trong phần này sử dụng contract SimpleStorage trong khoá Solidity Basics: tại đây
Final Code: tại đây
- Compile contract:
forge compile
- Format code:
forge fmt
- Run local Anvil chain:
anvil
Có 2 cách để deploy:
- Với
create
forge create SimpleStorage --rpc-url http://127.0.0.1:8545 --interactive
forge create SimpleStorage --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
Cách sử dụng --interactive
tốt hơn, không lưu private key dưới dạng plain text trong terminal.
- Với
script
forge script script/DeploySimpleStorage.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
- Tạo account mới để lưu trữ private key:
cast wallet import <ACCOUNT_NAME> --interactive
- Xem danh sách các account đã tạo
cast wallet list
- Chuyển hex value thành decimal value:
cast --to-base {hex} dec
- Send transaction:
cast send <CONTRACT_ADDRESS> <FUNCTION_SIGNATURE> <ARGS> --rpc-url <YOUR_RPC_URL> --account <ACCOUNT_NAME>
- Call transaction:
cast call <CONTRACT_ADDRESS> <FUNCTION_SIGNATURE> <ARGS> --rpc-url <YOUR_RPC_URL>
-
Ví dụ về contract đã được deploy và verify: Simple Storage
-
Sử dụng ENV file để bảo mật các giá trị riêng tư: ENV Example
-
Sử dụng ENV Variables trong terminal:
source .env
forge script script/DeploySimpleStorage.s.sol --rpc-url ${SEPOLIA_RPC_URL} --account <ACCOUNT_NAME> --broadcast
- Verify contract:
forge verify-contract <CONTRACT_ADDRESS> <PATH>:<CONTRACT_NAME> --rpc-url ${SEPOLIA_RPC_URL} --etherscan-api-key ${ETHERSCAN_API_KEY}
- Verify contract khi deploy contract:
forge script script/DeploySimpleStorage.s.sol --rpc-url ${SEPOLIA_RPC_URL} --account <ACCOUNT_NAME> --broadcast --verify --etherscan-api-key ${ETHERSCAN_API_KEY}
Lưu ý là nếu muốn verify contract trong khi deploy thì chỉ có thể sử dụng script bởi vì khi deploy thì script sẽ chứa luôn đường dẫn đến file của contract.
Code khởi đầu sử dụng contract Crowdfunding
trong khoá Blockchain Basics: tại đây
Final code: tại đây
Contract đã được deploy và verify: tại đây
Testing smart contract Solidity là quá trình kiểm tra, xác minh tính đúng đắn, an toàn và hiệu quả của mã nguồn smart contract được viết bằng ngôn ngữ lập trình Solidity. Quá trình này bao gồm việc viết và chạy các tập lệnh kiểm thử (test scripts) để mô phỏng các tình huống khác nhau mà hợp đồng thông minh có thể gặp phải trong thực tế. Mục tiêu của việc testing là đảm bảo rằng smart contract hoạt động chính xác theo yêu cầu, không có lỗi, và an toàn trước các lỗ hổng bảo mật.
Xem top những dự án blockchain/web3 bị hack: https://rekt.news/leaderboard/
-
Ngăn ngừa lỗi: Smart contract quản lý các giao dịch và tài sản trên blockchain, nơi mà bất kỳ lỗi nào cũng có thể gây ra thiệt hại tài chính lớn. Testing giúp phát hiện và sửa chữa các lỗi trong mã nguồn trước khi triển khai lên blockchain.
-
Đảm bảo bảo mật: Một khi smart contract đã được triển khai, nó không thể bị thay đổi. Nếu tồn tại lỗ hổng bảo mật, kẻ tấn công có thể lợi dụng để chiếm đoạt tài sản hoặc phá hoại hệ thống. Testing giúp phát hiện và loại bỏ các lỗ hổng này trước khi hợp đồng được sử dụng thực tế.
-
Xác minh tính đúng đắn của logic: Smart contract thường chứa các logic phức tạp để thực hiện các chức năng như quản lý tài sản, thực hiện giao dịch, hoặc tuân thủ các quy định. Testing giúp xác minh rằng logic này hoạt động đúng như mong đợi trong mọi tình huống, bao gồm cả các tình huống ngoại lệ.
-
Tối ưu hóa chi phí gas: Việc thực thi smart contract trên blockchain yêu cầu trả phí gas. Testing có thể giúp phát hiện các đoạn mã không tối ưu, từ đó giảm chi phí thực thi hợp đồng.
-
Tuân thủ các tiêu chuẩn và quy định: Một số smart contract cần phải tuân thủ các tiêu chuẩn cụ thể (ví dụ như ERC-20 cho token trên Ethereum). Testing giúp đảm bảo rằng hợp đồng đáp ứng các tiêu chuẩn này.
-
Tránh chi phí phát sinh không cần thiết: Việc sửa lỗi sau khi smart contract đã được triển khai có thể rất tốn kém, do phải triển khai lại hợp đồng và chi trả thêm phí gas. Testing giúp giảm thiểu khả năng xảy ra những sai lầm tốn kém này.
-
Tăng độ tin cậy: Một smart contract đã được kiểm tra kỹ lưỡng sẽ tạo dựng được sự tin tưởng từ phía người dùng, nhà đầu tư, và các bên liên quan khác. Điều này là rất quan trọng trong môi trường blockchain, nơi mà niềm tin vào tính toàn vẹn và an toàn của mã nguồn là yếu tố sống còn.
Việc testing smart contract Solidity là bước thiết yếu để đảm bảo rằng hợp đồng hoạt động đúng và an toàn, góp phần bảo vệ tài sản và xây dựng niềm tin trong cộng đồng người dùng blockchain.
- Unit:
- Kiểm tra một phần cụ thể trong code.
- Integration:
- Kiểm tra hoạt động của code với các phần khác trong dự án.
- Forked:
- Kiểm tra code với giả lập môi trường thực tế.
- Staging:
- Kiểm tra code trong một môi trường thực tế (testnet) nhưng không phải production (mainnet).
Hoặc mình hay gọi là Libraries, hoặc một Project,... sao cũng được.
Để cài đặt một dependency thì bạn hãy chạy lệnh forge install
. Tuy nhiên, bạn nên nhớ chỉ định version để code trong phần này và code của bạn match với nhau. Vì sau này có thể có nhiều bản cập nhận cho dependencies chúng ta muốn cài đặt, và nếu không chỉ định version thì mặc định cài bản mới nhất.
forge install <GITHUB_LINK>:<VERSION> --no-commit
Docs: tại đây
Thư viện forge
có thể tự remap dependencies (ánh xạ lại các phụ thuộc) để import links gọn hơn. Bằng cách chạy forge remappings
bạn có thể nhìn thấy những remap tự động.
$ forge remappings
Kết quả:
forge-std/=lib/forge-std/src/
Remappings trên có ý nghĩa là:
- Để import từ
forge-std
chúng ta có thể ghi:import "forge-std/Contract.sol";
chứ không cần phảiimport "lib/forge-std/src/Contract.sol
;
Bạn có thể tự custom remapping những phần phụ thuộc mà bạn muốn bằng cách thêm trường remappings
vào foundry.toml
:
remappings = ["@chainlink=lib/chainlink", "@solmate-utils/=lib/solmate/src/utils/"]
Nếu bạn chưa quen với việc testing (trong bất kỳ ngôn ngữ hay công nghệ nào). Thì bên trong test file sẽ có các function (hoặc method) được gọi là những test case.
Trong Foundry, các Test File sẽ được đặt trong folder test/
và được kết thúc bằng .t.sol
.
Bất kỳ contract nào có tên khởi đầu với chữ test
sẽ được thư viện forge xem là một test case và chạy nó.
Để chạy test bạn sử dụng command:
forge test
Để chạy một test case nhất định:
forge test --match-test <TEST_NAME>
Alias: --mt
Để chạy một file test nhất định:
forge test --math-path <PATH_TO_FILE>
Alias: --mp
Nếu trong test file có function console.log()
thì có thể dùng để xem log ra những biến (hoặc kết quả) mà mình muốn.
Để xem log thì hãy thêm cờ -v
vào lệnh commnand forge test
. Dưới đây là các level logs, mỗi level cao hơn sẽ có các tính năng của các level thấp hơn.
- Mức 2 ( -vv) : Logs được emmitted khi chạy tests.
- Mức 3 ( -vvv) : Stack traces (như mình hay nói là "luồng chạy") sẽ được hiển thị cho những test case nào thất bại.
- Mức 4 ( -vvvv) : Hiển thị stack traces cho mọi luồng chạy, bao gồm setup, test thành công và test thất bại.
Fork Test hay còn gọi là mô phỏng môi trường. Giúp việc chạy test trên một bản sao ảo của blockchain tại một thời điểm cụ thể. Điều này giúp bạn có một môi trường testing gần giống với thực tế.
Khi mà chúng ta chạy lệnh forge test
bên trên, forge sẽ tự động tạo cho chúng ta một môi trường local Anvil Chain và chạy những test case của chúng ta trên đó.
Để chạy fork test chúng ta chỉ cần thêm flag --fork-url
và khai báo RPC_URL của EVM chain mà bạn muốn.
forge test --fork-url <RPC_URL>
Nếu chúng ta chạy fork test thì chúng ta có thể tương tác với những hợp đồng đã được deploy. Còn nếu không? những hợp đồng chúng ta cần dưới local để tương tác thì chúng ta có thể thông qua một Mock Contract.
Mock Contract tạo ra một phiên bản giả lập của một contract nào đó mà chúng ta cần. Mock contract mô phỏng hành vi của contract thật (có thể đã được deploy hoặc của một hệ thống khác).
Mock contract giúp chúng ta cô lập unit test dưới local, chỉ tập trung test một test case duy nhất mà không ảnh hưởng nhiều bởi các yếu tố bên ngoài.
Nếu sử dụng mock contract được, thì chúng ta không cần chạy fork test với hợp đồng đã được deploy, giúp tiết kiệm thời gian.
Mock contract giúp tạo ra những tình huống mà hiện tại trong thực tế, contract được mock (được deploy) không tồn tại tình huống đó để chúng ta chạy test case cho tình huống đó. Ví dụ: giá thực tế của ETH là 3000USD/ETH, nhưng chúng ta muốn test trường hợp giá ETH là 30,000USD/ETH thì chúng ta sử dụng mock test cho các aggregator contract thì mới làm được điều đó.
Chạy lệnh
forge coverage
giúp chúng ta đo lường mức độ bao phủ của các test case đối với toàn bộ mã nguồn.
- Line Coverage: Số dòng mã được thực thi.
- Branch coverage: Số nhánh điều kiện được kiểm tra.
- Function coverage: Số hàm được gọi trong quá trình kiểm thử.
Nếu quá khó để handle thì chúng ta chỉ cần quan tâm đến function coverage, test đầy đủ hết đầu ra và đầu vào của một function.
Contract trong phần này là contract chúng ta đã hoàn thiện trong phần trước, đã được mình verify và deploy tại đây.
Giao diện đã được hoàn thiện và deploy: https://crowdfunding-interface.vercel.app/
Lưu ý: Yêu cầu tiên quyết trong phần này là bạn phải có kiến thức về việc phát triển Front-end với thư viện React. Mình sẽ dùng ngôn ngữ TypeScript trong phần này, nhưng nếu bạn có sử dụng JavaScript thì cũng có thể follow không vấn đề gì cả.
Những thư viện và công cụ được sử dụng trong phần này:
- Ngôn ngữ: TypeScript
- Library: React
- Build Tool: Vite
- CSS: TailwindCSS
- Icon: Lucide Icon
- Ethers
Final Code: tại đây
Trong phần này, chúng ta sẽ xây dựng một contract phục vụ cho việc chơi xổ số với các tính năng như sau:
- Cho phép người chơi tham gia vào cuộc chơi mà một ngày sẽ xổ số 1 lần (có thể thiết lập thời gian).
- Khi đến thời gian cho phép, contract sẽ tự động được kích hoạt và sẽ tìm ra 1 người chiến thắng bằng Chainlink VRF.
Layouts of Contract:
// Layout of Contract:
// version
// imports
// errors
// interfaces, libraries, contracts
// Type declarations
// State variables
// Events
// Modifiers
// Functions
// Layout of Functions:
// constructor
// receive function (if exists)
// fallback function (if exists)
// external
// public
// internal
// private
// view & pure functions