Know transferFrom() in ERC-20
สรุปการทำงานของฟังก์ชัน approve() และ transferFrom() ตามมาตรฐาน ERC-20 (อาจจะไม่ใหม่นัก แต่แนวคิดก็ยังใช้ได้อยู่)
TL;DR: ฟังก์ชัน transferFrom() เป็นฟังก์ชันที่ออกแบบไว้ให้ ผู้ที่ถูก approve เป็นคน execute ให้ทำงาน ซึ่งสามารถกำหนดได้ว่า ผู้ส่ง tokens คือใคร และ ผู้รับ tokens คือใคร
Introduction
มาตรฐาน ERC-20 เป็นมาตรฐาน token บน Ethereum ที่มีการกำหนดฟังก์ชันการทำงานแบ่งเป็น 6 functions และ 2 events ที่ต้องมีใน contract ตามรายละเอียดดังนี้
contract ERC20Interface {
function totalSupply() public constant returns (uint);
function balanceOf(address tokenOwner) public constant returns (uint balance);
function allowance(address tokenOwner, address spender) public constant returns (uint remaining);
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);
event Transfer(address indexed from, address indexed to, uint tokens);
event Approval(address indexed tokenOwner, address indexed spender, uint tokens);
}
เมื่อต้องการโอน tokens เราสามารถใช้ฟังก์ชัน transfer(address recipient, uint256 amount) เพื่อที่จะใช้ส่งจำนวน tokens ไปยัง address ปลายทางที่กำหนด (recipient) ได้ ดังตัวอย่าง source code ด้านล่างนี้
สังเกตเห็นว่าในฟังก์ชัน transfer() เราไม่ได้กำหนดว่า tokens ต้นทางที่หักออกนั้น ไม่ได้บอกว่า หัก tokens ออกจากต้นทางไหน ซึ่งจริง ๆ แล้ว tokens ต้นทาง (หรือเจ้าของบัญชี) นั้น ก็คือ คนที่เป็นคนสั่ง execute ฟังก์ชันนี้นั่นเอง ซึ่งสามารถอ่านค่าจาก address ของเจ้าบัญชี ผ่านตัวแปร msg.sender
นั่นก็คือ เจ้าของ tokens กับ คน execute ฟังก์ชัน เป็นคนเดียวกัน ไม่มีประเด็นอะไรเกี่ยวกับสิทธิ์ในการตัด tokens (เพราะเป็นคนเดียวกัน)
คราวนี้ ถ้าเราต้องทำหน้าที่เป็นตัวกลาง ที่ต้องการระบุว่า โอน tokens จากใคร ไปหาใคร ก็ต้องไปเรียกใช้ ฟังก์ชัน transferFrom() แต่ปัญหาคือ ถ้าคน execute ฟังก์ชัน เป็นคนละคนกับ เจ้าของ tokens ต้นทางในการตัด tokens เราจะจัดการสิทธิ์ได้อย่างไร?
Why transferFrom?
สมมุติว่า sender ต้องการโอน 10 tokens ไป ยัง recipient โดยกำหนดให้ tokenOwner (อาจเป็นคน deploy smart contract) เป็นคนสั่งให้ทำงาน (เสมือนเป็นคนกลางสั่งโอน tokens) ไม่ใช่ เจ้าตัว (sender) สั่งโอน tokens เอง ดังนั้น sender ต้องอนุญาตให้ tokenOwner ดำเนินการก่อน
เมื่อเจ้าของ tokens ต้นทาง กับ คนเรียกใช้ฟังก์ชันเป็นคนละคนกัน แสดงว่าจำเป็นต้องมีการขออนุมัติ (approve) ก่อน นั่นหมายความว่า ตอน approve คนที่ execute ฟังก์ชัน (msg.sender) คือ เจ้าของ tokens จะต้องเรียกใช้ฟังก์ชัน approve(tokenOwner, 10) และ sender คือ เจ้าของ token (หรือ tokenOwner อาจจะเป็นผู้ที่ deploy smart contract) ดังรูป
เมื่อสั่ง approve แล้ว จะมีการกำหนดค่า allowances[owner][spender] = 10 เพื่อให้ tokenOwner สามารถสั่ง โอน tokens จาก sender ไปยัง recipient ได้ โดย owner คือ sender (ผู้ที่ execute ฟังก์ชัน) และ spender คือ tokenOwner ผู้ถูก approve ให้สามารถ โอน tokens ได้
เมื่อ approve แล้ว ก็มาถึง transferFrom() โดย
- sender คือเจ้าของ tokens
- tokenOwner คือ ผู้ที่ execute ฟังก์ชันนี้
- recipient คือ ผู้รับ tokens
tokenOwner ก็สามารถสั่งโอน tokens จาก sender ไปยัง recipient ได้ ตามที่ระบุไว้ใน approve ผ่านตัวแปร allowances
ถ้าไม่ได้ขอ approve ไว้ก่อน การเรียกใช้ transferFrom จะถูก Revert ไม่สามารถดำเนินการได้
ข้อสังเกต:
1. ใน source code จะไม่มีการตรวจสอบยอดคงเหลือ (balance) ว่า ถ้า tokens ไม่พอ แล้วไป approve เวลาโอน tokens ไปจริง ๆ tokens จะสามารถติดลบได้หรือไม่?
ตอบ: ไม่สามารถทำได้ เนื่องจาก การ ลบค่า ทำผ่านฟังก์ชัน .sub() ที่เป็น safeMath library ที่จะตรวจสอบว่าไม่สามารถติดลบได้ ถ้าติดลบก็จะ revert การทำงาน และยกเลิกฟังก์ชันที่ execute มาก่อนหน้าทั้งหมด
2. tokenOwner ไม่จำเป็นต้องเป็นคนที่ deploy smart contract ได้ไหม?
ตอบ: ได้ แต่ยกตัวอย่างแบบนี้ เพื่อให้เข้าใจบทบาทหน้าที่ได้ง่าย
Credit: Concept เรื่องนี้มี มานานแล้ว ต้องขอบคุณ Nattapon Nimakul จาก Kulap ที่มาช่วย ให้ความกระจ่างในตอนนั้น ตั้งแต่สมัย solidity 0.5.x (แต่ผมเพิ่งมาว่างได้เขียนบันทึกเก็บไว้ ตอนนี้ solidity ไป 0.7.x นี้ละ)
ERC-20 reference: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol