Devahoy Logo
PublishedAt

Foundry

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

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

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

Step 1 - สร้างโปรเจ็ค

เริ่มต้นสร้างโปรเจ็คขึ้นมา ผมตั้งชื่อว่า greeter เป็น Contract get set message ธรรมดานะครับ

Terminal window
forge init greeter

ตัว Foundry (Forge) จะทำการ initial Project มาให้เรา ลองเปิดโฟลเดอร์ขึ้นมาดู โครงสร้าง (ผมใช้ VS Code)  จะประกอบไปด้วย

  • src - โฟลเดอร์หลักของ Contract (ถ้า Hardhat ก็จะเป็นโฟลเดอร์ Contract)
  • test - โฟลเดอร์สำหรับ testing
  • foundry.toml - เป็นเหมือน configuration file คล้ายๆ hardhat.config.js

ข้างในโปรเจ็ค มี Contract Counter.sol มาให้ รวมถึงไฟล์เทส Counter.t.sol

1
// SPDX-License-Identifier: UNLICENSED
2
pragma solidity ^0.8.13;
3
4
contract Counter {
5
uint256 public number;
6
7
function setNumber(uint256 newNumber) public {
8
number = newNumber;
9
}
10
11
function increment() public {
12
number++;
13
}
14
}

ลองรันคำสั่ง build

Terminal window
forge build

หากเราดูผลลัพธ์ การ compile จะเห็นว่าเร็วมากๆ

Terminal window
[] 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

Greeter.sol
1
//SPDX-License-Identifier: Unlicense
2
pragma solidity ^0.8.17;
3
4
contract Greeter {
5
string private greeting;
6
7
constructor(string memory _greeting) {
8
greeting = _greeting;
9
}
10
11
function greet() public view returns (string memory) {
12
return greeting;
13
}
14
15
function setGreeting(string memory _greeting) public {
16
greeting = _greeting;
17
}
18
19
}

ทำการเพิ่มไฟล์ Test ชื่อ test/Greeter.t.sol

test/Greeter.t.sol
1
// SPDX-License-Identifier: UNLICENSED
2
pragma solidity ^0.8.13;
3
4
import 'forge-std/Test.sol'
5
import '../src/Greeter.sol'
6
7
contract GreeterTest is Test {
8
Greeter public greeter;
9
10
function setUp() public {
11
greeter = new Greeter("Hello World!");
12
}
13
14
function testGreeting() public {
15
assertEq(greeter.greet(), "Hello World!");
16
}
17
18
function testSetGreeting() public {
19
string memory msg = "Ahoy!";
20
greeter.setGreeting(msg);
21
assertEq(greeter.greet(), msg);
22
}
23
24
}

Remappings

หากใครเจอปัญหา เวลาเปิดไฟล์เทสแล้วหาไฟล์ forge-std/Test.sol ไม่เจอ

แสดงว่าเรายังไม่ได้ remappings ให้มันครับ ทำการ remappings ด้วยคำสั่ง forge remappings และก็ทำการเซฟไว้ที่ไฟล์ชื่อ remappings.txt

Terminal window
forge remappings > remappings.txt

ลองรัน Test

Terminal window
forge test

เราสามารถดู traces ได้ด้วย ใช้ -vvv เฉพาะ test ที่ fail หรือใช้ -vvvv test ทั้งหมด

Terminal window
forge test -vvvv

อีกอันที่ชอบคือ test —gas-report โดยไม่ต้องลง plugin เพิ่ม

Terminal window
forge test --gas-report

Step 3- Local Node

ลองรัน Local Node ด้วย anvil (เทียบกับ Hardhat คือ npx hardhat node)

Terminal window
anvil

แถมเราสามารถ fork network ได้ด้วย

Terminal window
anvil -fork-url <FORK_URL>

ทีนี้ เราก็มี Local Node แล้วที่ 127.0.0.1:8545

ลอง Deploy local และทดสอบ connect RPC ดู (ใช้ Private Key ที่ได้จาก Local node เป็น private key ที่ไม่ปลอดภัย ห้ามเอาไปใช้ Production เด็ดขาด)

Terminal window
forge create src/Greeter.sol:Greeter \
--constructor-args "Hello" \
--private-key=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

จะได้ผลลัพธ์ที่ Deploy แบบนี้ (0x5FbDB2315678afecb367f032d93F642f64180aa3 คือ Contract Address ที่เรา deployed ไป)

Terminal window
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Transaction hash: 0x48a5999df83ed50edcbc76c1997d1e41b08b31896392d829c0420a9c2cd82b4b

ถ้าเราไปดูในหน้า Local node ของเรา จะเห็นว่ามี Contract ถูก deploy แสดงใน log

Terminal window
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

Terminal window
cast gas-price

ดู block number

Terminal window
cast block-number

ทดลอง เรียก greet() จาก Contract ที่เรา Deploy ด้วยคำสั่ง cast call

Terminal window
cast call 0x5FbDB2315678afecb367f032d93F642f64180aa3 "greet()(string)"

ถ้าสังเกต รูปแบบมันจะเป็นแบบนี้

Terminal window
cast call <CONTRACT_ADDRESS> <FUNCTION(RETURN)>

จะเห็นว่าได้ผลลัพธ์ เป็นค่า “Hello” ที่เราทำการ setup ที่ contructor ตอน Deploy Contract.

ลอง setGreeting ดู เปลี่ยนจาก cast call เป็น cast send

Terminal window
cast send 0x5FbDB2315678afecb367f032d93F642f64180aa3 \
"setGreeting(string)" "Hello World!!" --from \
0x70997970C51812dc3A010C7d01b50e0d17dc79C8

ใน localhost ผม send tx ได้ อาจจะเพราะใช้ account default แต่บน testnet ผมลอง send tx แล้วได้ Error แบบ issue นี้เลย (ขอทิ้ง issue ไว้ก่อน เดี๋ยวกลับมาดูว่า แก้ปัญหายังไง)

Terminal window
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

1
[profile.default]
2
src = 'src'
3
out = 'out'
4
libs = ['lib']
5
eth-rpc-url = "https://rpc.sepolia.org/"
6
7
# See more config options https://github.com/foundry-rs/foundry/tree/master/config

รันคำสั่ง

Terminal window
forge create --rpc-url <your_rpc_url> \
--private-key <your_private_key> \
src/Greeter.sol:Greeter

หรือเราสามารถใช้ Environment Variable ได้ เช่น ~/.zshrc หรือ ~/.bashrc

Terminal window
FOUNDRY_ETH_RPC_URL=https://rpc.sepolia.org/
FOUNDRY_PRIVATE_KEY=<YOUR_PRIVATE_KEY>
ETHERSCAN_API_KEY=<ETHERSCAN_API>

หรืออีกวิธี สร้าง .env ขึ้นมา ในโฟลเดอร์ของโปรเจ็คเรานี่แหละ ( source .env)

1
FOUNDRY_ETH_RPC_URL=https://rpc.sepolia.org/
2
FOUNDRY_PRIVATE_KEY=<YOUR_PRIVATE_KEY>
3
4
ETHERSCAN_API_KEY=<ETHERSCAN_API>

สุดท้ายผมลอง Deploy ด้วย Config ด้านบน ก็จะเหลือคำสั่งแค่นี้

Terminal window
forge create src/Greeter.sol:Greeter \
--constructor-args "Hello World" \
--private-key=$FOUNDRY_PRIVATE_KEY

เมื่อ Deploy เสร็จ ก็จะเห็นผลลัพธ์

Terminal window
[] 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 แบบนี้

Terminal window
forge create \
src/Greeter.sol:Greeter \
--constructor-args "Hello World" \
--private-key=$FOUNDRY_PRIVATE_KEY \
--verify \
--etherscan-api-key $ETHERSCAN_API_KEY

ผลลัพธ์ก็จะเป็นประมาณนี้

Terminal window
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

แต่ถ้ามา 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 เราจะใช้คำสั่ง

Terminal window
cast abi-encode "constructor(string)" "Hello World"

จะได้ผลลัพธ์แบบนี้

Terminal window
0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b48656c6c6f20576f726c64000000000000000000000000000000000000000000

ทำการ Verify Contract

Terminal window
forge verify-contract \
--chain-id 11155111 \
--constructor-args "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000b48656c6c6f20576f726c64000000000000000000000000000000000000000000" \
--etherscan-api-key $ETHERSCAN_API_KEY \
0xd42391926c4a6a5c5a3987658e18d4f1236be2a4 \
src/Greeter.sol:Greeter

หรือ

Terminal window
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

1
[rpc_endpoints]
2
sepolia = "${SEPOLIA_RPC_URL}"
3
bsc = "${BSC_RPC_URL}"
4
5
[etherscan]
6
sepolia = { key = "${ETHERSCAN_API_KEY}" }
7
bsc = { key = "${BSC_API_KEY}", url = "https://api.bscscan.com/api"}

ดู config

Terminal window
forge config

นอกจากนั้น พวก Library ต่างๆ ถ้าเป็น Hardhat ส่วนใหญ่ใช้ OpenZeppelin ถ้าใน Foundry เห็นหลายๆคนนิยมใช้ Solmate และติดตั้งผ่าน forge ได้เลย

Terminal window
forge install transmissions11/solmate

จริงๆ Foundry ก็ติดตั้ง OpenZeppelin ได้เหมือนกัน

Terminal window
forge install OpenZeppelin/openzeppelin-contracts

Happy Coding ❤️

Reference

Foundry BookA book on all things FoundryFoundry Book

Authors
avatar

Chai Phonbopit

เป็น Web Dev ในบริษัทแห่งหนึ่ง ทำงานมา 10 ปีกว่าๆ ด้วยภาษาและเทคโนโลยี เช่น JavaScript, Node.js, React, Vue และปัจจุบันกำลังสนใจในเรื่องของ Blockchain และ Crypto กำลังหัดเรียนภาษา Rust

Related Posts