วิธีการ Swap Token กับ Uniswap

Warodom Werapun
http://warodom.werapun.com
4 min readOct 27, 2021

--

swapExactInputMultiHop() #DevSession

Introduction to Uniswap

Uniswap เป็น แหล่งแลกเปลี่ยนเหรียญคริปโตขนาดใหญ่ ที่ใช้กลไกการกำหนดราคาแบบ AMM (Automated Market Maker) เป็นการเทรด เหรียญ ผ่าน Liquidity Pool หรือ คนที่เอาคู่เหรียญมาฝากไว้ในระบบเพื่อได้ค่าตอบแทน นอกจากนี้ คนที่มาใช้งานผ่านระบบ Uniswap ไม่จำเป็นจะต้องยืนยันตัวเอง (KYC) ก่อนเข้าใช้งานอีกด้วย

Uniswap มีหลาย component ประกอบด้วย uniswap-v3-core, uniswap-v3-sdk, uniswap-v3-interfaces, และ uniswap-v3-periphery โดยบทความนี้ ใช้ code ของ contract จาก uniswap-v3-periphery เป็นหลัก

ในบทความนี้ จะแนะนำวิธีการเขียนโปรแกรมในการ swap เหรียญ โดย Swap จาก DAI => USDC => WETH9 อ้างอิงจาก​ https://docs.uniswap.org/protocol/guides/swaps/multihop-swaps ซึ่งมี 2 ฟังก์ชันคือ swapExactInputMultiHop() กับ swapExactOutputMultiHop() ซึ่งตัวอย่างนี้ จะสนใจเฉพาะ ​swapExactInputMultiHop() ก่อน เนื่องจากเข้าใจได้ง่ายกว่า สามารถทำได้ดังนี้

Token Swap Steps

Preparation

  1. เริ่มจากการเปลี่ยน ETH ให้เป็น DAI เพื่อจะได้มี DAI ไว้เริ่มต้นในการ Swap

2. ต้องมี LP เพื่อใช้ในการ แลกคู่เหรียญ (Liquidity Provider)

ต้อง Approve คู่เหรียญ เพื่อรองรับ การ swap เหรียญให้เรียบร้อย กด Approve และ วางคู่ DAI — USDC, USDC-WETH9 ตามลำดับ บางคู่เหรียญถ้ามี LP อยู่แล้ว ก็ไม่จำเป็นต้อง วางคู่เหรียญ​​ โดยเฉพาะใน mainnet จะมีคู่เหรียญ อยู่มากมาย

Prepare hardhat project environment

สร้าง Hardhat project

  1. สร้าง hardhat project โดย npx hardhat และ เลือก typescript project
  2. ให้ copy contract ใน folder contracts จาก periphery repo (git clone https://github.com/Uniswap/v3-periphery.git) มาใส่ใน contracts ของ project หลัก
  3. เพิ่ม dependency ดังนี้ ใน periphery repo ลงใน package.json
“dependencies”: {
“@openzeppelin/contracts”: “3.4.1-solc-0.7–2”,
“@uniswap/lib”: “⁴.0.1-alpha”,
“@uniswap/v2-core”: “1.0.1”,
“@uniswap/v3-core”: “1.0.0”,
“base64-sol”: “1.0.1”,
“hardhat-watcher”: “^2.1.1”
},

4. เนื่องจากมี contracts หลายไฟล์ มีปัญหาเรื่อง stack too deep จึงต้องมีการ configure hardhat.config.ts ให้ optimize solidity และ configure rinkeby network

solidity: {
version: "0.7.6",
settings: {
optimizer: {
enabled: true,
runs: 2_000,
},
metadata: { bytecodeHash: "none", },
},
},

...
networks: {
rinkeby: {
url: process.env.RINKEBY_URL,
accounts:process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
},
}

Start swapping

สามารถ Swap ได้ 2 แบบ

  1. เรียกจาก contract ที่ swap จาก router อีกที

จากรูป

  • เร่ิมต้น ต้อง approve DAI ให้กับ MySwap contract ก่อน (นอกทำ นอก contract)
  • จากนั้น มีการโอนจำนวน Dai ที่ต้องการโอนเก็บไว้ใน MySwap ผ่าน transferForm เพื่อให้ MySwap สามารถจัดการเหรียญได้
  • MySwap contract approve สิทธิ์ในการจัดการเหรียญให้กับ swapRouter
  • swapRouter เตรียม parameters ต่าง ๆ ก่อนเรียกใช้ swapRouter.exactInput() เพื่อ swap เหรียญ

2. เรียก swap จาก router โดยตรง

  • approve DAI ให้ swapRouter มีสิทธิ์ในการจัดการ
  • หลังจากเตรียม parameters ต่างเรียบร้อย เรียกใช้ swapRouter.exactInput() จะเห็นได้ว่าขั้นตอนต่าง ๆ จะสั้นกว่า เพราะทำผ่าน Router โดยตรง ไม่ต้องเรียกผ่าน contract อีกรอบ

1. Swap Token โดยเรียกจาก Contract ที่ swap จาก ​Router อีกที

  • Deploy script เพื่อ เรียกใช้ contract

ตัวอย่างนี้ มีการเปลี่ยนแปลง ในส่วนของ ชื่อ Contract, Router address และ ​Token address ที่ มีการระบุลงใน contract เพื่อให้ง่ายแก่การเรียกใช้งาน และ สอดคล้องเครือข่ายของ Rinkeby และ deadline ที่ให้เวลาในการ swap เสร็จก่อน

/scripts/deploy-swap-contract.ts

ผลการทำงาน

ได้ผลการทำงานจาก rinkeby.etherscan ดังนี้ ภาพ โดย transaction จะไม่เรียงลำดับ

2. Swap Token โดยเรียกจาก Router โดยตรง

วิธีที่จะเรียกผ่าน Router โดยตรง ทำให้ลดขั้นตอนการทำงานและประหยัดแก๊สมากกว่า

ผลการทำงาน

ได้ผลการทำงานจาก rinkeby.etherscan ดังนี้ ภาพ โดย transaction จะไม่เรียงลำดับ

ข้อผิดพลาดที่อาจจะเจอ

ถ้ามา Deploy uniswap contract เอง คิดว่า นักพัฒนาหลายคน จะต้องเจอ ข้อผิดพลาดต่าง ๆ ลักษณะนี้แน่นอน ซึ่งเวลาทดสอบใน testnet นั้น หาข้อผิดพลาดยาก… (มาก) เพราะ หลาย ๆ contract ไม่ได้ verify contract source ไว้ (ไม่สามารถดู source code ที่ผิดพลาดใน tenderly.co ได้) รวมถึง Token economic ใน test net กับ main net ต่างกันมาก บาง Pool อาจจะไม่มีคู่เหรียญ และอื่น ๆ อีกมากมาย ซึ่ง เวลาแสดงข้อผิดพลาด หากเราไม่ได้ Deploy contract ทั้งหมด (ย้ำว่าทั้งหมด ตั้งแต่ swapRouter contract และอื่น ๆ ใน localhost เราจะไม่สามารถใช้ console.log ตรวจสอบข้อผิดพลาดได้ เพราะมันไม่ได้ run ใน localhost network)

reason: ‘cannot estimate gas; transaction may fail or may require manual gas limit’,
code: ‘UNPREDICTABLE_GAS_LIMIT’,
error: ProviderError: execution reverted: AS
reason: ‘cannot estimate gas; transaction may fail or may require manual gas limit’,
code: ‘UNPREDICTABLE_GAS_LIMIT’,
error: ProviderError: execution reverted: STF
reason: ‘cannot estimate gas; transaction may fail or may require manual gas limit’,
code: ‘UNPREDICTABLE_GAS_LIMIT’,
error: ProviderError: execution reverted

แนวทางแก้ปัญหา

ทางแก้ปัญหาจากข้อผิดพลาดข้างบน มีวิธีแก้ดังข้างล่างนี้

  • ตรวจสอบว่า Address ที่จะเอามา swap นั้น ถูกต้อง อยู่ในเครือข่ายที่เรียกใช้งาน (Rinkeby) จริง ๆ หรือไม่
  • จำนวน ETH ใส่ไป ต้องปรับให้อยู่ในรูปแบบของ 10¹⁸
  • อย่าลืม Approve contract (กรณีใช้ contract เรียก) หรือ Approve Router กรณี swap ผ่าน Router โดยตรง
  • มีคู่เหรียญที่ต้องการจะ Swap อยู่ใน ​Pool ไหม (มี Liquidity)
  • deadline ที่ใช้ มีเวลาเพียงพอที่จะให้ Transaction ทำงานได้สำเร็จ

Repo: https://github.com/wwarodom/myswap

ขอขอบคุณ Huzen Karode และ นายนราธร บุญเปี่ยม ที่ช่วยให้ข้อมูลในการเรียกใช้ contract swapExactInputMultiHop() จนสำเร็จ

--

--