ลองเขียนและ Deploy Smart Contract ด้วย Foundry
หลังจากที่หลายวันก่อนได้ลองใช้งาน Foundry และก็หัดใช้งานเบื้องต้นไป เผื่อจะเอามาแทนที่ Hardhat วันนี้วันหยุด ว่างๆ ก็เลยถือโอกาส ลองเล่น ลองเปลี่ยนมาลองใช้ Foundry ตั้งแต่เริ่ม เทส และ Deploy ดูว่าจะเป็นไง เลยกลายมาเป็นบทความนี้ครับ

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: UNLICENSED
pragma 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.50ms
Compiler run successful
Step 2 - สร้าง Contract
ผมทำการสร้าง Contract ขึ้นมาใหม่ ชื่อ Greeter.sol
อยู่ในโฟลเดอร์ src
//SPDX-License-Identifier: Unlicense
pragma 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: UNLICENSED
pragma 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: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Transaction 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_getTransactionByHash
eth_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 call
- เอาไว้ call contract (read-only)cast send
- sign และ send transaction
Step 5 - Deploy Testnet
ขั้นตอนนี้ ผมจะทำการ Deploy Testnet โดยใช้ Sepolia (หรือใครจะใช้ Testnet อื่นๆ ที่สะดวกก็ได้)

การ 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 skipped
Deployer: 0x5A65b0B75C3AfCF9F4c911f6c2Fc96e80486C3CE
Deployed to: 0xd42391926C4a6A5C5a3987658e18d4F1236Be2a4
Transaction 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/0xd42391926c4a6a5c5a3987658e18d4f1236be2a4
Contract 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
