ลองเขียนและ Deploy Smart Contract ด้วย Foundry

หลังจากที่หลายวันก่อนได้ลองใช้งาน Foundry และก็หัดใช้งานเบื้องต้นไป เผื่อจะเอามาแทนที่ Hardhat วันนี้วันหยุด ว่างๆ ก็เลยถือโอกาส ลองเล่น ลองเปลี่ยนมาลองใช้ Foundry ตั้งแต่เริ่ม เทส และ Deploy ดูว่าจะเป็นไง หลังจากได้ลองอ่าน Foundry Book พบว่ามันน่าสนใจมากๆ ตัว Foundry ให้คำนิยามตัวเองไว้ คือ Foundry is a blazing fast
Step 1 - สร้างโปรเจ็ค
เริ่มต้นสร้างโปรเจ็คขึ้นมา ผมตั้งชื่อว่า greeter เป็น Contract get set message ธรรมดานะครับ
forge init greeter
ตัว Foundry (Forge) จะทำการ initial Project มาให้เรา ลองเปิดโฟลเดอร์ขึ้นมาดู โครงสร้าง (ผมใช้ VS Code) จะประกอบไปด้วย
src
- โฟลเดอร์หลักของ Contract (ถ้า Hardhat ก็จะเป็นโฟลเดอร์ Contract)test
- โฟลเดอร์สำหรับ testingfoundry.toml
- เป็นเหมือน configuration file คล้ายๆ hardhat.config.js
ข้างในโปรเจ็ค มี Contract Counter.sol มาให้ รวมถึงไฟล์เทส Counter.t.sol
// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.13;
contract Counter { uint256 public number;
function setNumber(uint256 newNumber) public { number = newNumber; }
function increment() public { number++; }}
ลองรันคำสั่ง build
forge build
หากเราดูผลลัพธ์ การ compile จะเห็นว่าเร็วมากๆ
[⠢] Compiling...[⠰] Compiling 1 files with 0.8.19[⠔] Solc 0.8.19 finished in 83.50msCompiler run successful
Step 2 - สร้าง Contract
ผมทำการสร้าง Contract ขึ้นมาใหม่ ชื่อ Greeter.sol
อยู่ในโฟลเดอร์ src
//SPDX-License-Identifier: Unlicensepragma solidity ^0.8.17;
contract Greeter {string private greeting;
constructor(string memory _greeting) { greeting = _greeting; }
function greet() public view returns (string memory) { return greeting; }
function setGreeting(string memory _greeting) public { greeting = _greeting; }
}
ทำการเพิ่มไฟล์ Test ชื่อ test/Greeter.t.sol
// SPDX-License-Identifier: UNLICENSEDpragma solidity ^0.8.13;
import 'forge-std/Test.sol'import '../src/Greeter.sol'
contract GreeterTest is Test {Greeter public greeter;
function setUp() public { greeter = new Greeter("Hello World!"); }
function testGreeting() public { assertEq(greeter.greet(), "Hello World!"); }
function testSetGreeting() public { string memory msg = "Ahoy!"; greeter.setGreeting(msg); assertEq(greeter.greet(), msg); }
}
Remappings
หากใครเจอปัญหา เวลาเปิดไฟล์เทสแล้วหาไฟล์ forge-std/Test.sol
ไม่เจอ
แสดงว่าเรายังไม่ได้ remappings ให้มันครับ ทำการ remappings ด้วยคำสั่ง forge remappings
และก็ทำการเซฟไว้ที่ไฟล์ชื่อ remappings.txt
forge remappings > remappings.txt
ลองรัน Test
forge test
เราสามารถดู traces ได้ด้วย ใช้ -vvv เฉพาะ test ที่ fail หรือใช้ -vvvv test ทั้งหมด
forge test -vvvv
อีกอันที่ชอบคือ test —gas-report โดยไม่ต้องลง plugin เพิ่ม
forge test --gas-report
Step 3- Local Node
ลองรัน Local Node ด้วย anvil (เทียบกับ Hardhat คือ npx hardhat node)
anvil
แถมเราสามารถ fork network ได้ด้วย
anvil -fork-url <FORK_URL>
ทีนี้ เราก็มี Local Node แล้วที่ 127.0.0.1:8545
ลอง Deploy local และทดสอบ connect RPC ดู (ใช้ Private Key ที่ได้จาก Local node เป็น private key ที่ไม่ปลอดภัย ห้ามเอาไปใช้ Production เด็ดขาด)
forge create src/Greeter.sol:Greeter \--constructor-args "Hello" \--private-key=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
จะได้ผลลัพธ์ที่ Deploy แบบนี้ (0x5FbDB2315678afecb367f032d93F642f64180aa3 คือ Contract Address ที่เรา deployed ไป)
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3Transaction hash: 0x48a5999df83ed50edcbc76c1997d1e41b08b31896392d829c0420a9c2cd82b4b
ถ้าเราไปดูในหน้า Local node ของเรา จะเห็นว่ามี Contract ถูก deploy แสดงใน log
th_sendRawTransaction
Transaction: 0x48a5999df83ed50edcbc76c1997d1e41b08b31896392d829c0420a9c2cd82b4b Contract created: 0x5fbdb2315678afecb367f032d93f642f64180aa3 Gas used: 287234
Block Number: 1 Block Hash: 0x2d5cbb8add500db98c24ab20d70868944b6e813911f7f4490a3193499dd735f2 Block Time: "Sat, 25 Mar 2023 11:12:52 +0000"
eth_getTransactionByHasheth_getTransactionReceipt
Step 4 - Call RPC with Cast
ทดลอง Call RPC ตัว contract ที่เรา Deploy ด้วย cast ครับ ตัว cast เป็น Command Line ที่ให้เรา call RPC ได้
ถ้าเราไม่ได้ config ตัว rpc-url จะเป็น localhost:8545 สามารถกำหนด rpc url ได้ด้วย option --rpc-url=<RPC_URL>
ดู gas price
cast gas-price
ดู block number
cast block-number
ทดลอง เรียก greet() จาก Contract ที่เรา Deploy ด้วยคำสั่ง cast call
cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "greet()(string)"
ถ้าสังเกต รูปแบบมันจะเป็นแบบนี้
cast call <CONTRACT_ADDRESS> <FUNCTION(RETURN)>
จะเห็นว่าได้ผลลัพธ์ เป็นค่า “Hello” ที่เราทำการ setup ที่ contructor ตอน Deploy Contract.
ลอง setGreeting ดู เปลี่ยนจาก cast call เป็น cast send
cast send 0x5FbDB2315678afecb367f032d93F642f64180aa3 \"setGreeting(string)" "Hello World!!" --from \0x70997970C51812dc3A010C7d01b50e0d17dc79C8
ใน localhost ผม send tx ได้ อาจจะเพราะใช้ account default แต่บน testnet ผมลอง send tx แล้วได้ Error แบบ issue นี้เลย (ขอทิ้ง issue ไว้ก่อน เดี๋ยวกลับมาดูว่า แก้ปัญหายังไง)
cast send --interactive asks for sender address · Issue #4616 · foundry-rs/foundryComponent Cast Have you ensured that all of these are up to date? Foundry Foundryup What version of Foundry are you on? cast 0.2.0 (394f217 2023-03-21T00:11:00.708322Z) What command(s) is the bug i…GitHubfoundry-rs
สรุป
cast call
- เอาไว้ call contract (read-only)cast send
- sign และ send transaction
Step 5 - Deploy Testnet
ขั้นตอนนี้ ผมจะทำการ Deploy Testnet โดยใช้ Sepolia (หรือใครจะใช้ Testnet อื่นๆ ที่สะดวกก็ได้)
Sepolia FaucetA fast and reliable Ethereum Sepolia testnet faucet for blockchain developers.Sepolia Faucet
การ Deploy ก็เหมือนกับตอน deploy local แต่เพียงต้องเพิ่ม —rpc-url ด้วย สามารถเพิ่มเป็น option หรือกำหนดที่ไฟล์ foundry.toml ก็ได้ โดย foundry config สามารถใช้ไฟล์ที่ local folder ก็ได้ หรือจะเป็น global ก็เซฟไว้ที่ ~/.foundry/foundry.toml
ไฟล์ foundry.toml
[profile.default]src = 'src'out = 'out'libs = ['lib']eth-rpc-url = "https://rpc.sepolia.org/"
# See more config options https://github.com/foundry-rs/foundry/tree/master/config
รันคำสั่ง
forge create --rpc-url <your_rpc_url> \--private-key <your_private_key> \src/Greeter.sol:Greeter
หรือเราสามารถใช้ Environment Variable ได้ เช่น ~/.zshrc
หรือ ~/.bashrc
FOUNDRY_ETH_RPC_URL=https://rpc.sepolia.org/FOUNDRY_PRIVATE_KEY=<YOUR_PRIVATE_KEY>
ETHERSCAN_API_KEY=<ETHERSCAN_API>
หรืออีกวิธี สร้าง .env ขึ้นมา ในโฟลเดอร์ของโปรเจ็คเรานี่แหละ ( source .env)
FOUNDRY_ETH_RPC_URL=https://rpc.sepolia.org/FOUNDRY_PRIVATE_KEY=<YOUR_PRIVATE_KEY>
ETHERSCAN_API_KEY=<ETHERSCAN_API>
สุดท้ายผมลอง Deploy ด้วย Config ด้านบน ก็จะเหลือคำสั่งแค่นี้
forge create src/Greeter.sol:Greeter \--constructor-args "Hello World" \--private-key=$FOUNDRY_PRIVATE_KEY
เมื่อ Deploy เสร็จ ก็จะเห็นผลลัพธ์
[⠢] Compiling...No files changed, compilation skippedDeployer: 0x5A65b0B75C3AfCF9F4c911f6c2Fc96e80486C3CEDeployed to: 0xd42391926C4a6A5C5a3987658e18d4F1236Be2a4Transaction hash: 0x75136937dfa39abab6c6df24cb5d8be29f72c4139c1f4301a143fdb0f5878651
Transaction ที่ Deploy บน Sepolia Testnet
Step 6 - Verify Contract
จริงๆ เราสามารถ Verify พร้อมกับตอน Deploy เลยก็ได้ ถ้าเราใส่ option --verify
และ --etherscan-api-key
แบบนี้
forge create \src/Greeter.sol:Greeter \--constructor-args "Hello World" \--private-key=$FOUNDRY_PRIVATE_KEY \--verify \--etherscan-api-key $ETHERSCAN_API_KEY
ผลลัพธ์ก็จะเป็นประมาณนี้
Submitted contract for verification:Response: `OK`GUID: `qmaxxweh17paept166bvmbww8gpap7q4ixh3hs67aebkyyumhx`URL:https://sepolia.etherscan.io/address/0xd42391926c4a6a5c5a3987658e18d4f1236be2a4Contract verification status:Response: `NOTOK`Details: `Pending in queue`Contract verification status:Response: `OK`Details: `Pass - Verified`Contract successfully verified
- Link Etherscan - https://sepolia.etherscan.io/address/0xd42391926c4a6a5c5a3987658e18d4f1236be2a4#code
แต่ถ้ามา Verify ทีหลัง เราจำเป็นต้องมีค่าดังนี้
- Contract Address ที่เรา Deploy ไป
- Chain Id - chain ที่เรา จะ Verify
- Construct Args - เป็นแบบ ABI Code
- Etherscan API Key - ใช้ Account ของ Etherscan หลักได้เลย และขอ API Key ได้ฟรี
- Number of Optimization - default 200 ถ้าเราไม่ได้ปรับอะไร
- Compiler Version - ถ้าไม่ใส่ ตัว Foundry จะ detect ให้ เราสามารถกำหนด ให้ตรงกับที่เรา Build & Deploy ได้
ตัว abi code ของ constructor เราจะใช้คำสั่ง
cast abi-encode "constructor(string)" "Hello World"
จะได้ผลลัพธ์แบบนี้
0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b48656c6c6f20576f726c64000000000000000000000000000000000000000000
ทำการ Verify Contract
forge verify-contract \--chain-id 11155111 \--constructor-args "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b48656c6c6f20576f726c64000000000000000000000000000000000000000000" \--etherscan-api-key $ETHERSCAN_API_KEY \0xd42391926c4a6a5c5a3987658e18d4f1236be2a4 \src/Greeter.sol:Greeter
หรือ
forge verify-contract \ --chain-id 11155111 \ --constructor-args $(cast abi-encode "constructor(string)" "Hello World") \ --etherscan-api-key $FOUNDRY_ETHERSCAN_API_KEY \ 0xd42391926c4a6a5c5a3987658e18d4f1236be2a4 \ src/Greeter.sol:Greeter
🎉 จบแล้ว ลองไปเล่นกันดูนะครับ
สรุป
หลังจากลอง Workflow การพัฒนา การ Test การ Deploy ต่างๆ ก็รู้สึกว่าเร็วดี และก็ไม่ได้ยุ่งยากเท่าไหร่ มีติดปัญหาตรง Deploy และก็ Verify Contract นิดหน่อย พวกค่า environment variables ต่างๆ ใช้ prefix FOUNDRY_ กับบางค่าไม่ได้ด้วย (ไม่ตรงกับ Struct ของ Foundry) และก็เรื่อง foundry.toml ที่ยังไม่ได้ลองกำหนด Config ดีๆเลย แบบแยก chain แยก etherscan
[rpc_endpoints]sepolia = "${SEPOLIA_RPC_URL}"bsc = "${BSC_RPC_URL}"
[etherscan]sepolia = { key = "${ETHERSCAN_API_KEY}" }bsc = { key = "${BSC_API_KEY}", url = "https://api.bscscan.com/api"}
ดู config
forge config
นอกจากนั้น พวก Library ต่างๆ ถ้าเป็น Hardhat ส่วนใหญ่ใช้ OpenZeppelin ถ้าใน Foundry เห็นหลายๆคนนิยมใช้ Solmate และติดตั้งผ่าน forge ได้เลย
forge install transmissions11/solmate
จริงๆ Foundry ก็ติดตั้ง OpenZeppelin ได้เหมือนกัน
forge install OpenZeppelin/openzeppelin-contracts
Happy Coding ❤️
Reference
Foundry BookA book on all things FoundryFoundry Book
- Authors
-
Chai Phonbopit
เป็น Web Dev ในบริษัทแห่งหนึ่ง ทำงานมา 10 ปีกว่าๆ ด้วยภาษาและเทคโนโลยี เช่น JavaScript, Node.js, React, Vue และปัจจุบันกำลังสนใจในเรื่องของ Blockchain และ Crypto กำลังหัดเรียนภาษา Rust