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

Foundry Mar 25, 2023

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

ติดตั้งและลองใช้งาน Foundry
วันนี้ได้ลองหัดเล่น Foundry ซึ่งเคยติดตั้งไว้นานมากๆ แล้ว แต่ไม่ได้ใช้งานซักที ฮ่าๆ วันนี้ลองมานั่งอ่าน Foundry Book นั่งเล่นใหม่ พบว่ามันน่าสนใจมากๆ ตัว Foundry ให้คำนิยามตัวเองไว้ คือ Foundry is a blazing fast,
วิธีการติดตั้ง Foundry อ่านจากบทความก่อนหน้าได้

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

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

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

// 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 send --interactive asks for sender address · Issue #4616 · foundry-rs/foundry
Component 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…

สรุป

  • cast call - เอาไว้ call contract (read-only)
  • cast send - sign และ send transaction

Step 5 - Deploy Testnet

ขั้นตอนนี้ ผมจะทำการ Deploy Testnet โดยใช้ Sepolia (หรือใครจะใช้ Testnet อื่นๆ ที่สะดวกก็ได้)

Sepolia Faucet
A fast and reliable Ethereum Sepolia testnet faucet for blockchain developers.
ไปขอ 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 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

แต่ถ้ามา 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 Book
A book on all things Foundry

Tags

Chai Phonbopit

เป็น Web Dev ทำงานมา 10 ปีหน่อยๆ ด้วยภาษา JavaScript, Node.js, React, Vue และปัจจุบันกำลังสนใจ Web3, Crypto และ Blockchain เขียนบล็อกที่ https://devahoy.com