สร้าง ERC-20 Token ด้วย ink! Part 1/2
วันนี้มาลองสร้าง ERC-20 token ด้วย ink! ครับ ซึ่งถ้าหากใครไม่เคยเขียน ink! มาก่อน หรือภาษา Rust มาก่อน แนะนำอ่านบทความก่อนหน้านี้ที่ผมได้เขียนไว้ครับ ทดลองเขียน Smart Contract ด้วย ink! + Rust + Substrate
ซึ่งตัว ERC-20 Token หลักๆ ก็จะเหมือนกับ ERC-20 ของฝั่ง EVM นั่นแหละครับ โดยเนื้อหา Tutorial ผมอ้างอิงจาก Build an ERC-20 token contract
เนื้อหามี 2 ตอนนะครับ
- สร้าง ERC-20 Token ด้วย ink! Part 1/2 (บทความนี้)
- สร้าง ERC-20 Token ด้วย ink! Part 2/2
เตรียมตัวกันก่อน
สำหรับใครที่เพิ่งมาอ่านบทความนี้ ก็ลองเตรียมความพร้อม Rust และ Toolchain รวมถึง Substrate Contract Node จากบทความนี้ได้ครับ เตรียมเครื่องมือสำหรับ Substrate Development และควรมีพื้นฐาน Rust และ ink! เบื้องต้นครับ แนะนำอ่านบทความก่อนหน้าของผมได้ครับ สำหรับคนที่ยังไม่เคยเขียน ink!
ERC-20 Standard
เราจะอ้างอิงตัว ERC-20 นี้นะครับ ซึ่งเป็น standard ที่ใช้มากที่สุดใน Ethereum blockchain หน้าตาคร่าวๆ ของมันก็มีดังนี้
contract ERC20Interface {
// Storage Getters
function totalSupply() public view returns (uint);
function balanceOf(address tokenOwner) public view returns (uint balance);
function allowance(address tokenOwner, address spender) public view returns (uint remaining);
// Public Functions
function transfer(address to, uint tokens) public returns (bool success);
function approve(address spender, uint tokens) public returns (bool success);
function transferFrom(address from, address to, uint tokens) public returns (bool success);
// Contract Events
event Transfer(address indexed from, address indexed to, uint tokens);
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}
หลักๆ ก็คือ เป็น Interace ที่บอกให้รู้ไว้ว่า Token มีความสามารถอะไรบ้าง เช่น
totalSupply
- แสดง totalSupply ได้balanceOf
- เพื่อดู balance ของ address นั้นๆtransfer
,trasferFrom
- สำหรับ ส่ง token ระหว่าง address เป็นต้น
จะเห็นว่า Interface ด้านบนเป็น Solidity แต่ว่าสำหรับ ink! ที่เขียนด้วย Rust ตัว return ทีเป็น bool
ใน Rust ก็จะใช้ Result
แทนนะครับ ส่วนที่เหลือ ก็แทบจะเหมือนๆกัน
Step 1 - Create new contract
สร้างโปรเจ็คขึ้นมา โดยใช้ Cargo Contract
cargo contract new erc20
จะได้โฟลเดอร์ชื่อ erc20 เปลี่ยนข้างในไฟล์ lib.rs
เป็น ด้านล่าง
#![cfg_attr(not(feature = "std"), no_std)]
use ink_lang as ink;
#[ink::contract]
mod erc20 {
use ink_storage::{
traits::SpreadAllocate,
Mapping,
};
/// Create storage for a simple ERC-20 contract.
#[ink(storage)]
#[derive(SpreadAllocate)]
pub struct Erc20 {
/// Total token supply.
total_supply: Balance,
/// Mapping from owner to number of owned tokens.
balances: Mapping<AccountId, Balance>,
}
impl Erc20 {
/// Create a new ERC-20 contract with an initial supply.
#[ink(constructor)]
pub fn new(initial_supply: Balance) -> Self {
// Initialize mapping for the contract.
ink_lang::utils::initialize_contract(|contract| {
Self::new_init(contract, initial_supply)
})
}
/// Initialize the ERC-20 contract with the specified initial supply.
fn new_init(&mut self, initial_supply: Balance) {
let caller = Self::env().caller();
self.balances.insert(&caller, &initial_supply);
self.total_supply = initial_supply;
}
/// Returns the total token supply.
#[ink(message)]
pub fn total_supply(&self) -> Balance {
self.total_supply
}
/// Returns the account balance for the specified `owner`.
#[ink(message)]
pub fn balance_of(&self, owner: AccountId) -> Balance {
self.balances.get(owner).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use ink_lang as ink;
#[ink::test]
fn new_works() {
let contract = Erc20::new(777);
assert_eq!(contract.total_supply(), 777);
}
#[ink::test]
fn balance_works() {
let contract = Erc20::new(100);
assert_eq!(contract.total_supply(), 100);
assert_eq!(contract.balance_of(AccountId::from([0x1; 32])), 100);
assert_eq!(contract.balance_of(AccountId::from([0x0; 32])), 0);
}
}
}
ตัว Source Code จากเว็บ Substrate ink! workshop
ทดสอบ verify ดูว่า test ผ่านมั้ย
cargo +nightly test
รอ cargo compile ซักนิด ถ้าไม่มี dependencies เลย มันก็จะไปดาวน์โหลดและ compiled ครับ ถ้าเทสผ่าน จะได้ผลลัพธ์ประมาณนี้
Running unittests lib.rs (target/debug/deps/erc20-879749f53b41eace)
running 2 tests
test erc20::tests::new_works ... ok
test erc20::tests::balance_works ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
เมื่อเทสผ่าน ลอง build เป็น WebAssembly contract ดู
cargo +nightly contract build
เมื่อ build เสร็จเราจะได้ไฟล์ erc20.contract
ในโฟลเดอร์ target/ink
สามารถ นำไฟล์นี้ไป deploy ผ่าน Contract UI ดูได้ครับ
สามารถกลับไปอ่านบทความก่อนหน้านี้ เรื่องของการ Deploy ผ่าน Contracts UI ได้ที่บทความ ทดลองเขียน Smart Contract ด้วย ink! + Rust + Substrate
Step 2 - Transfer tokens
ณ ตอนนี้ ตัว ERC-20 contract ของเรา จะมีแค่ 1 user account ที่เป็นเจ้าของ total_supply ของ token อยู่ วิธีที่จะทำให้ contract สมบูรณ์ ก็คือ ต้องสามารถ transfer tokens ให้ accounts อื่นๆ ได้
Logic ในการ transfer คือ
- ดูค่า balance ของ account
from
และto
- เช็คดูว่าเงินของ
from
น้อยกว่าvalue
หรือเปล่า? มีเงินแค่ 100 เดียว แต่จะส่ง 1แสน ต้องส่งไม่ได้ :) - หักเงินจาก
from
และเพิ่มเงินให้to
ด้วยจำนวนvalue
เพิ่ม Error type ที่บรรทัดที่ 8 ต่อจาก use ink_storage::
ในไฟล์ lib.rs
#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
/// Return if the balance cannot fulfill a request.
InsufficientBalance,
}
pub type Result<T> = core::result::Result<T, Error>;
เขียน function transfer()
เพื่อให้ contract caller สามารถส่ง token ให้ account อื่นได้
วางต่อจากฟังค์ชั่น balance_of()
ได้เลย
#[ink(message)]
pub fn transfer(&mut self, to: AccountId, value: Balance) -> Result<()> {
// เหมือน msg.sender
let from = self.env().caller();
self.transfer_from_to(&from, &to, value)
}
fn transfer_from_to(
&mut self,
from: &AccountId,
to: &AccountId,
value: Balance,
) -> Result<()> {
let from_balance = self.balance_of_impl(from);
// 1. เช็คว่า ถ้า balance ของ from น้อยกว่า value ที่จะส่ง ต้องส่งไม่ได้
if from_balance < value {
return Err(Error::InsufficientBalance)
}
// 2. ลด balance ของ from
self.balances.insert(from, &(from_balance - value));
let to_balance = self.balance_of_impl(to);
// 3. เพิ่ม balance ให้ to
self.balances.insert(to, &(to_balance + value));
Ok(())
}
#[inline]
fn balance_of_impl(&self, owner: &AccountId) -> Balance {
self.balances.get(owner).unwrap_or_default()
}
ลองเทส ดูว่า ว่าผลลัพธ์เป็นยังไง
cargo +nightly test
Step 3 - Events
ต่อมาเราจะสร้าง Event โดยปกติแล้ว ตัว contract ไม่สามารถ return value ตรงๆ ได้ (ต้องรอ confirm) ฉะนั้น วิธีที่จะทำให้ client รู้ได้ ก็คือรู้ผ่าน Event นั่นเอง
Event ใน ink! เราจะกำหนดเป็น strcut ครับ โดยใช้ attribute #[ink(event)]
เพิ่ม struct ต่อจากบรรทัด pub type Result<T> = core::result::Result<T, Error>;
ได้เลย
#[ink(event)]
pub struct Transfer {
#[ink(topic)]
from: Option<AccountId>,
#[ink(topic)]
to: Option<AccountId>,
value: Balance,
}
การ Emit event เราจะกำหนด ให้ emit 2 ส่วนคือ
- ตอนที่
new
- เพื่อ initialize contract (ขั้นตอนนี้ไม่มีto
ฉะนั้นto
จึงเป็นNone
ได้) - ทุกๆ ครั้งที่เรียก function
transfer_from_to
เพิ่ม emit event ที่ function new_init()
fn new_init(&mut self, initial_supply: Balance) {
let caller = Self::env().caller();
self.balances.insert(&caller, &initial_supply);
self.total_supply = initial_supply;
Self::env().emit_event(Transfer {
from: None,
to: Some(caller),
value: initial_supply,
});
}
และเพิ่ม emit ใน transfer_from_to()
แบบนี้
fn transfer_from_to(
&mut self,
from: &AccountId,
to: &AccountId,
value: Balance,
) -> Result<()> {
let from_balance = self.balance_of_impl(from);
// 1. เช็คว่า ถ้า balance ของ from น้อยกว่า value ที่จะส่ง ต้องส่งไม่ได้
if from_balance < value {
return Err(Error::InsufficientBalance);
}
// 2. ลด balance ของ from
self.balances.insert(from, &(from_balance - value));
let to_balance = self.balance_of_impl(to);
// 3. เพิ่ม balance ให้ to
self.balances.insert(to, &(to_balance + value));
self.env().emit_event(Transfer {
from: Some(*from),
to: Some(*to),
value,
});
Ok(())
}
ลองเพิ่มเทส transfer ใน mod tests
ดู
#[ink::test]
fn transfer_works() {
let mut erc20 = Erc20::new(100);
assert_eq!(erc20.balance_of(AccountId::from([0x0; 32])), 0);
assert_eq!(erc20.transfer(AccountId::from([0x0; 32]), 10), Ok(()));
assert_eq!(erc20.balance_of(AccountId::from([0x0; 32])), 10);
}
ทดสอบรันเทสอีกครั้ง
cargo +nightly test
จะมี 3 tests และผ่านหมด แสดงว่า เราทำถูกต้อง
running 3 tests
test erc20::tests::new_works ... ok
test erc20::tests::balance_works ... ok
test erc20::tests::transfer_works ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
🎉 เบื้องต้น ตอนนี้เราก็สามารถทำเหรียญ ERC-20 token โดยใช้ ink! ได้แล้ว แต่ความสามารถของมันก็ยังไม่ได้มีแค่ transfer ต่อไป Part 2 เราจะมาต่อด้วยเรื่องของการ Approve token และการ transfer เหรียญจาก account คนอื่น (ขอสิทธ์แล้ว) ว่าทำยังไงนะครับ
สำหรับตอนนี้ แนะนำว่า ลองทบทวน และลองอ่านโค๊ดดูครับ และพยายามทำความเข้าใจดูว่าโค๊ดมันทำงานยังไง ถ้าอ่านไม่เข้าใจ แนะนำ ลองไปอ่าน Rust Book บท 1-6 เพิ่มเติมครับ รวมถึง ink! Doc เพิ่มเติม เรื่องของ Attributes นะครับ
ไว้มาต่อ Part 2 เร็วๆนี้ครับ
Happy Coding ❤️
References
- Authors
- Name
- Chai Phonbopit
- Website
- @Phonbopit