Building Proxy NFT For Repeatable Use Contract

Natthapach Anuwattananon
4 min readSep 13, 2022

--

สวัสดีครับ บทความนี้เป็นบทความแรกของซีรี่ย์ Building NFT Smart Contract for Real World Usecase นะครับ ซึ่งจะมาเล่าเทคนิคการสร้าง smart contract NFT สำหรับนำมาประยุกต์กับ realworld usecase กันครับ ตั้งใจว่าจะเขียนออกมา 3 บทความครับ

สำหรับบทความนี้จะเริ่มต้นด้วยเรื่องเบาๆกันก่อนครับ นั้นคือเทคนิคการใช้ Proxy หลายๆตัว กับ implementation contract ตัวเดียว เพื่อลด cost ในการสร้าง smart contract ครับ

Proxy for Repeatable Use

หลายๆคนอาจคุ้นเคยการใช้ proxy สำหรับการ upgrade smart contract โดยการมี 1 proxy contract และหลาย implementation contract ใช่ไหมครับ

using proxy for upgradeable

แต่มีอีกเทคนิคในการใช้ proxy คือการใช้ 1 implementation contract กับหลาย proxy contract แทนครับ

เทคนิคนี้ใช้สำหรับ project ที่ต้องมีการสร้าง smart contract ตัวใหม่ๆ แต่อยากให้มี implementation เหมือนเดิม เช่น Music NFT ของ sound.xyz ที่จะสร้าง smart contract สำหรับศิลปินแต่ละคน แต่ว่า implementation ของแต่ละ contract ก็เหมือนเดิม

using proxy for repeatable use implementation

ข้อดีของเทคนิคนี้คือจะทำให้ประหยัดค่า gas ตอน deploy ไปได้มาก เพราะ proxy contract นั้นมีขนาดเล็กมาก เมื่อเทียบกับ implementation contract ครับ

ในบทความนี้จะเล่าถึงเทคนิคการ deploy proxy และเชื่อมกับ implementation contract ในแบบต่างๆ ทั้งใน hardhat, contract และ node.js ครับ

What things inside Openzeppelin Proxy?

หลายๆคนน่าจะคุ้นเคยกับการ deploy upgradeable contract ด้วย hardhat script และ openzeppelin plugin แบบโค้ดด้านล่างนี้

ก่อนจะสร้าง script สำหรับ deploy proxy เองนั้น คำถามคือใน function deployProxy นั้นทำอะไรให้เราบ้าง?

How Openzeppelin Proxy work?

ก่อนจะไปแกะโค้ดของ upgrade plugin เราลองมา recap กันก่อนว่า proxy นั้นทำงานอย่างไรครับ

ทุกคนที่เคยใช้ proxy น่าจะเคยได้ยินว่า proxy นั้นใช้ delegate call ในการ forward call ที่เข้ามาเพื่อไปยัง implementation contract นะครับ (ในบทความนี้ผมจะไม่ลงลึกเรื่อง delegate call นะครับ ไม่งั้นจะหลุดประเด็นเกินไป)

proxy delegate call

แต่ว่า Proxy เองก็มี method ด้วยเหมือนกัน แล้วตัว Proxy เองจะรู้ได้ยังไง ว่า function call ไหนควร forward ไปยัง implementation contract หรือ execute function ของตัวเอง

วิธีแก้นั้นง่ายมาก ยังไงคนที่ควร call function ของ proxy ได้จะมีแค่ owner ของ contract เท่านั้น ถ้าเช่นนั้นเราก็ระบุเงื่อนไขไปเลยว่า

ทุกๆ call ที่เป็นของ owner จะ execute function ของ proxy เท่านั้น ไม่ forward ไปยัง implementation contract

แต่ก็เกิดปัญหาตามมา ซึ่งก็คือถ้าแล้วอย่างนั้นเจ้าของ contract ก็จะเรียก contract ตัวเองไม่ได้สิ เพราะทุกๆคอลที่เข้าไปจะไม่ forward ไปยัง implementation contract เลย

Openzeppelin แก้ปัญหานี้ด้วยการสร้าง smart contract ตัวกลางขึ้นมา เรียกว่า ProxyAdmin ซึ่งจะทำหน้าที่เป็น owner ของ proxy แทน หากต้อง call function ของ Proxy ก็ให้ทำการเรียกผ่าน ProxyAdmin แทนครับ

proxy admin as owner of proxy

กลับไปที่คำถามของเรากันครับ ว่า deployProxy นั้นทำอะไรให้เราบ้าง? ซึ่งโดยหลักๆแล้วจะมีดังนี้ครับ ตามคอนเซปด้านบนเลย

  1. deploy ProxyAdmin
  2. deploy Proxy
  3. deploy Implementation
  4. integrate ProxyAdmin — Proxy — Implementation

หลังจากรู้แบบนี้แล้ว เราก็สามารถสร้าง deploy proxy script ของเราเองได้แล้วครับ

Deploying only Proxy

สำหรับเทคนิคการ deploy proxy นั้น ในบทความนี้จะเล่าวิธีการ implement 3 วิธีด้วยกันครับ ซึ่งแต่ละวิธีก็ขึ้นอยู่กับ requirement และ system design ครับ

  1. Deploy proxy script with Hardhat: วิธีนี้จะเป็นการสร้างใน hardhat script ครับ ซึ่งเป็นวิธีที่ basic ที่สุด แต่ก็จะเหมาะกับ developer ใช้งานเพียงอย่างเดียว
  2. Deploy proxy with Contract Factory: วิธีนี้จะเป็นการสร้าง smart contract สำหรับการ deploy Proxy โดยเฉพาะครับ ซึ่งวิธีนี้ก็จะเหมาะกับ public use เป็นอย่างมาก
  3. Deploy proxy with Node.js API: วิธีสุดท้ายจะเป็นการสร้าง API สำหรับการ deploy proxy ครับ ซึ่งจะเหมาะสำหรับการ integrate เข้าการส่วนอื่นๆของระบบเราครับ

Deploy Proxy Script with Hardhat

เริ่มจากวิธีการที่ basic สุดกันก่อน นั้นก็คือการ deploy ด้วย hardhat script ครับ ด้านล่างเป็นโค้ดตัวเต็ม ซึ่งผมจะค่อยๆอธิบายต่อจากโค้ดนี้นะครับ

Preparing initialize call data

ทุกคนที่เคยใช้ upgradeable contract มาน่าจะพอรู้อยู่แล้วว่า upgradeable contract นั้นจะไม่ใช้ constructor function แต่จะใช้ initialize function แทน และในการ deploy Proxy นั้น หลังจาก deploy ก็จะต้องทำการเรียก initialize เพื่อทำการ initial contract ด้วยเช่นกัน

ดังนั้นแล้วสิ่งแรกที่เราต้องการคือ data สำหรับการ call initialize function ครับ ซึ่งสามารถสร้างได้จาก interface ของ implementation contract และ arguments สำหรับการคอล initialize ครับ

Acquire ProxyAdmin, Proxy Contract Factory

ขั้นตอนต่อมา ในเมื่อเราต้องการสร้าง contract ProxyAdmin และ Proxy ดังนั้นสิ่งที่เราต้องการคือ ContractFactory ของทั้งสองตัวนี้ครับ

แต่โดยปกติแล้วหากเป็น contract ที่เราเขียนเอง hardhat จะทำให้เราสามารถเรียกมาผ่าน ethers.getContractFactory() ได้ แต่ในกรณีนี้ทั้งสองตัวเป็น contract ใน library ของ openzeppelin ครับ จะให้ไปก็อปโค้ดจาก github openzeppelin มาทั้งก้อนก็ดูจะไม่ใช่วิธีที่สวยงามเท่าไหร่

มาถึงตรงนี้ต้องอธิบายเพิ่มเติมครับ ว่าตอนที่ hardhat นั้น compile smart contract จะได้สิ่งที่เรียกว่า artifacts ของแต่ละ contract ออกมาครับ ซึ่งภายในก็จะประกอบไปด้วย ABI และ bytecode ของ contract นั้นๆครับ (ลองสังเกตดูได้นะครับ หลังจากรัน hardhat compile และ จะมี folder atrifacts เพิ่มขึ้นมา)

ประเด็นอยู่ที่ bytecode ครับ ซึ่ง bytecode นี้แหละคือ contract ของเราที่ถูก compile เป็น binary แล้ว เพียงแค่มี bytecode นี้ก็จะสามารถ deploy contract ใหม่ได้ครับ

การที่เราสามารถเรียกใช้งานและ deploy contract ที่อยู่ใน library ของ openzeppelin ได้ ก็เพราะว่ามี artifact อยู่ใน library นี้แหละครับ

เอาล่ะ พอเรารู้แบบนี้แล้ว ก็แค่ไปตามหาว่า artifact ที่ว่าถูกเก็บไว้ที่ไหน ซึ่งผมงมหามาแล้วครับ

หลังจากที่ได้ abi และ bytecode มาแล้ว ก็สามารถนำมาสร้าง Contract Factory ได้แล้วครับ

Deploy ProxyAdmin and Proxy

หลังจากเตรียมทุกอย่างที่จำเป็นพร้อมแล้ว ที่เหลือก็แค่ deploy ProxyAdmin และ Proxy ครับ ส่วนการเชื่อมต่อทั้งสามส่วนเข้าด้วยกัน ก็เพียงแค่ระบุ address ของ contract แต่ละตัวตอนที่ deploy Proxy ได้เลยครับ

วิธีการนี้เหมาะสำหรับ developer เป็นคนใช้งานและ deploy เองเท่านั้นครับ ซึ่งหากต้องการการใช้งานแบบ public use หรือ integrate กับระบบอื่นๆ ต้องใช้วิธีด้านล่างแทนครับ

Deploy Proxy with Contract Factory

Contract Factory เป็นการสร้าง smart contract ไว้สำหรับ deploy proxy contract โดยเฉพาะ ซึ่งวิธีนี้เหมาะแก่การใช้งานแบบ public use เช่น ให้คนทั่วไปสามารถสร้าง contract ใหม่ได้เอง เป็นต้นครับ

พอเป็น smart contract เหมือนกันแล้ว อะไรๆก็ค่อนข้างง่ายขึ้นเยอะเลยครับ สิ่งสำคัญของการทำ Factory แบบนี้คือ ห้าม import implementation contract เข้ามา

สาเหตุก็เนื่องมาจาก smart contract บน EVM โดยทั่วไปแล้วมีข้อจำกัดเรื่อง contract size อยู่ที่เพียง 24.576 kb เท่านั้นครับ

หาก implementation contract เรานั้นใหญ่มาก สมมติมีขนาดมากขึ้น 24 kb แล้ว หาก Factory import เข้ามาใช้งาน จะทำให้ Factory เรามีขนาดเกิด limit แน่นอนครับ

นอกจากนี้ตัวอย่างด้านบนได้แยก config สำหรับ contract ที่จะสร้างใหม่ ออกเป็น 2 แบบ คือ config ที่ผู้ใช้งานสามารถระบุเข้ามาได้ (price) และ config ที่เจ้าของ Factory เท่านั้นที่สามารถปรับได้ (cap) ลองนำไปประยุกต์ใช้กันดูนะครับ

Deploy Proxy with Node.js

หากต้องการนำการสร้าง smart contract ใหม่ เข้ามาอยู่ใน automatation pipeline 2 วิธีด้านบนยังไม่ค่อยตอบโจทย์สักเท่าไหร่ครับ วิธีสุดท้ายที่จะแนะนำคือการนำ deployment logic มาสร้างเป็น API เพื่อไป integrate กับระบบส่วนอื่นครับ

ขั้นตอนการทำนั้นจะคล้ายกับใน hardhat script ครับ เพียงแต่ในกรณีนี้เราจะย้ายมาทำใน node.js และใช้ ethers.js ในการทำเองทั้งหมด

จุดนึงที่ผมเพิ่มเข้ามาให้คือการสร้าง TransactionRequest ก่อน จากนั้นค่อยนำไปเรียก signer.sendTransaction ครับ ซึ่งเทคนิคนี้สามารถนำไปประยุกต์ใช้กับ design system ที่ต้องการแยกส่วน logic สำหรับการเตรียมข้อมูล และสำหรับการติดต่อกับ blockchain ออกจากกันได้

เพียงเท่านี้ก็จะได้ API สำหรับการ deploy proxy มาใช้เองในระบบแล้วครับ

สำหรับบทความนี้ก็จบเพียงเท่านี้ครับ เทคนิคการใช้ proxy นี้ และวิธีการ implement ทั้งสามแบบสามารถนำไปประยุกต์ใช้กับแต่ละ requirement และ system design ได้ครับ

ส่วนบทความต่อไปจะพาไปรู้จักและ implement Gasless payment sytem เพื่อแก้ปัญหาเรื่องค่า gas ที่เป็นอุปสรรคต่อการสร้าง usecase ใน real world กันครับ

--

--