آموزش ساخت توکن های ERC20 در سالیدیتی
اکنون آمادهایم تا اولین توکن ERC20 خود را بسازیم!
توکن های ERC20 معمولاً یک نام (name) و یک نماد (symbol) دارند. برای مثال، توکن ApeCoin دارای نام “ApeCoin” و نماد “APE” است. از آنجا که نام و نماد توکن معمولاً تغییر نمیکنند، آنها را در سازنده (constructor) تعریف میکنیم و نیازی به تابعی برای تغییرشان در آینده نخواهیم داشت. همچنین آنها را بهصورت عمومی (public) تعریف میکنیم تا هرکسی بتواند نام و نماد توکن را مشاهده کند:
1 2 3 4 5 6 7 8 9 |
contract ERC20 { string public name; string public symbol; constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; } } |
ذخیره موجودی کاربران
گام بعدی این است که موجودی کاربران مختلف را ذخیره کنیم:
1 2 3 4 5 6 7 8 9 10 11 |
contract ERC20 { string public name; string public symbol; mapping(address => uint256) public balanceOf; constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; } } |
ما از عبارت balanceOf
استفاده میکنیم، زیرا بخشی از استاندارد ERC20 است. طبق این استاندارد، کاربران میتوانند تابع balanceOf
را فراخوانی کرده، یک آدرس وارد کنند و ببینند که آن آدرس چه مقدار توکن دارد.
در حال حاضر، موجودی همه صفر است؛ بنابراین باید راهی برای ایجاد توکن (Minting) وجود داشته باشد. در این مرحله، اجازه میدهیم که تنها آدرس خاصی — یعنی همان کسی که قرارداد را مستقر کرده — بتواند توکن تولید کند.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
contract ERC20 { string public name; string public symbol; mapping(address => uint256) public balanceOf; address public owner; constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; owner = msg.sender; } function mint(address to, uint256 amount) public { require(msg.sender == owner, "only owner can create tokens"); balanceOf[owner] += amount; } } |
در عمل، رایج است که تابع mint()
دو آرگومان دریافت کند: to
(آدرسی که توکن ها به آن تعلق میگیرد) و amount
(تعداد توکن هایی که قرار است ساخته شوند). این ساختار به سازنده قرارداد اجازه میدهد که توکن را برای حساب های دیگر نیز تولید کند. اما برای ساده سازی مثال، در اینجا تابع mint()
فقط به فردی که قرارداد را ساخته اجازه میدهد که توکن ها را برای خودش بسازد.
برای اینکه بدانیم چه تعداد توکن تا الان ایجاد شدهاند، استاندارد ERC20 نیاز به یک متغیر عمومی یا تابع به نام totalSupply
دارد که تعداد کل توکنهای موجود را برمیگرداند.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
contract ERC20 { string public name; string public symbol; mapping(address => uint256) public balanceOf; address public owner; uint256 public totalSupply; constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; owner = msg.sender; } function mint(address to, uint256 amount) public { require(msg.sender == owner, "only owner can create tokens"); totalSupply += amount; balanceOf[owner] += amount; } function transfer(address to, uint256 amount) public { require(balanceOf[msg.sender] >= amount, "you aint rich enough"); require(to != address(0), "cannot send to address(0)"); balanceOf[msg.sender] -= amount; balanceOf[to] += amount; } } |
اگر تا به حال از یک توکن ERC20 در کیف پول خود استفاده کرده باشید، احتمالاً دیدهاید که گاهی موجودی توکن ها به صورت اعشاری نمایش داده میشود. اما چگونه چنین چیزی ممکن است، در حالی که متغیرهای uint256
توانایی نگهداری اعداد اعشاری را ندارند؟
بزرگ ترین عددی که یک uint256
میتواند نگه دارد، چنین عدد بزرگی است:
115792089237316195423570985008687907853269984665640564039457584007913129639935
بیایید کمی عدد را کاهش دهیم تا قابل درک تر شود:
10000000000000000000000000000000000000000000000000000000000000000000000000000
اگر فرض کنیم توکن ما ۱۸ رقم اعشار دارد، آنوقت ۱۸ صفر سمت راست این عدد بهعنوان بخش اعشاری در نظر گرفته میشوند:
10000000000000000000000000000000000000000000000000000000000.000000000000000000
در نتیجه، اگر توکن ERC20 ما دارای ۱۸ رقم اعشار باشد، میتوانیم حداکثر این تعداد کوین کامل داشته باشیم:
10000000000000000000000000000000000000000000000000000000000
که صفرهای سمت راست آن بهعنوان اعشار در نظر گرفته میشوند. این مقدار معادل ۱۰ اُکتودسیلیون (octodecillion) کوین است. و برای کسانی که با این اعداد بسیار بزرگ آشنا نیستند، این عدد معادل است با:
۱ کوادریلیون × ۱ کوادریلیون × ۱ کوادریلیون × ۱ تریلیون
۱۰ اُکتودسیلیون باید برای اکثر کاربردها کافی باشد — حتی برای کشورهایی که وارد وضعیت اَبرتورم میشوند.
واحدهای ارزی همچنان اعداد صحیح (integer) هستند، اما این واحدها اکنون مقادیر بسیار کوچکی از کوین را نمایش میدهند.
عدد ۱۸ بهعنوان رقم اعشار، استانداردی رایج در بین توکنها است، اما برخی توکنها مانند USDC تنها از ۶ رقم اعشار استفاده میکنند.
تعداد اعشار یک توکن نباید تغییر کند؛ این فقط یک تابع ساده است که مشخص میکند توکن چند رقم اعشار دارد و صرفاً همان عدد را برمیگرداند.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
contract ERC20 { string public name; string public symbol; mapping(address => uint256) public balanceOf; address public owner; uint8 public decimals; uint256 public totalSupply; constructor(string memory _name, string memory _symbol, uint8 decimals) { name = _name; symbol = _symbol; decimals = 18; owner = msg.sender; } function mint(address to, uint256 amount) public { require(msg.sender == owner, "only owner can create tokens"); totalSupply += amount; balanceOf[owner] += amount; } } |
اگر با دقت به توضیحات ارائهشده توجه کرده باشید، در این بخش با نکتهای ظریف روبهرو هستیم. نوع داده ای که برای تعداد اعشار (decimals) استفاده میشود، uint8
است و نه uint256
. دلیل این انتخاب آن است که نوع uint8
تنها قادر است مقادیری بین ۰ تا ۲۵۵ را در خود جای دهد، در حالی که یک متغیر از نوع uint256
میتواند عددی با حداکثر ۷۷ رقم را نمایش دهد (در صورت تمایل، میتوانید با شمارش صفرهای اعداد ذکرشده در مثالهای قبلی، این موضوع را بررسی کنید).
از آنجا که برای داشتن یک واحد کامل از توکن، نمیتوان بیش از ۷۷ رقم اعشار در نظر گرفت، استاندارد ERC20 استفاده از نوع uint8
را برای تعیین تعداد اعشار الزامی کرده است. این تصمیم کاملاً منطقی است، چرا که حتی در کاربردهای خاص نیز نیاز به بیش از ۷۷ رقم اعشار وجود ندارد و چنین حالتی عملاً غیرممکن است.
حالا بیایید تابع transfer
را دوباره به قراردادمان اضافه کنیم.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
contract ERC20 { string public name; string public symbol; mapping(address => uint256) public balanceOf; address public owner; uint8 public decimals; uint256 public totalSupply; constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; decimals = 18; owner = msg.sender; } function mint(address to, uint256 amount) public { require(msg.sender == owner, "only owner can create tokens"); totalSupply += amount; balanceOf[owner] += amount; } function transfer(address to, uint256 amount) public { require(balanceOf[msg.sender] >= amount, "you aint rich enough"); require(to != address(0), "cannot send to address(0)"); balanceOf[msg.sender] -= amount; balanceOf[to] += amount; } } |
transfer
) یک خط جدید اضافه شده است:
1 |
require(to != address(0), "cannot send to address(0)"); |
address(0)
) متعلق به هیچ کاربری نیست. بنابراین، هر توکنی که به این آدرس ارسال شود، برای همیشه غیرقابل استفاده خواهد بود. بهعبارت دیگر، ارسال توکن به این آدرس معادل با از بین رفتن آن است. طبق عرف در استاندارد ERC20، زمانی که توکن ها به آدرس صفر منتقل میشوند، در واقع بهنوعی “سوخته” شدهاند و باید از مقدار کل عرضه (totalSupply) نیز کسر شوند. به همین دلیل، بهتر است برای چنین عملکردی یک تابع مجزا بهنام burn
تعریف شود و از ارسال مستقیم به آدرس صفر جلوگیری گردد.
مفهوم Allowance (مقدار مجاز) در سالیدیتی
اکنون به یکی از مفاهیم مهم در استاندارد ERC20 میپردازیم: Allowance.
Allowance یا «مقدار مجاز» این امکان را فراهم میکند که یک آدرس مشخص، تا سقف تعیینشدهای از توکن های شما را خرج کند.
ممکن است این سؤال پیش بیاید که چرا باید اجازه دهید شخص دیگری توکن های شما را خرج کند؟ پاسخ کامل به این سؤال مفصل است، اما خلاصهاش این است که انتقال توکن های ERC20 صرفاً با اجرای یک تابع و تغییر مقدار در یک mapping انجام میشود. بنابراین، برخلاف دنیای واقعی، شما «دریافت فیزیکی» ندارید؛ بلکه فقط مالکیت توکن ها از طریق آدرس تغییر میکند.
در این میان، قراردادهای هوشمند (smart contracts) نمیتوانند مانند انسان ها وضعیت حساب ها را بررسی کرده و از انتقال توکن به خود مطلع شوند. به همین دلیل، در طراحی قراردادهای هوشمند، یک الگوی مرسوم استفاده میشود:
-
ابتدا کاربر باید با استفاده از تابع
approve
به قرارداد هوشمند اجازه دهد تا تعداد مشخصی از توکن هایش را برداشت کند. -
سپس قرارداد هوشمند با فراخوانی تابع
transferFrom
توکن ها را از حساب کاربر برداشت میکند.
این الگو یکی از اجزای حیاتی در تعامل قراردادهای هوشمند با توکن های ERC20 است و استفاده گستردهای در پروژه های دیفای (DeFi)، صرافی های غیرمتمرکز و سایر برنامه های غیرمتمرکز دارد.
در ادامه، ساختار داده ای برای ذخیره مقدار مجاز (allowance) و تابع approve
برای تعیین این مقدار را به قرارداد خود اضافه خواهیم کرد.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
contract ERC20 { string public name; string public symbol; mapping(address => uint256) public balanceOf; address public owner; uint8 public decimals; uint256 public totalSupply; // owner -> spender -> allowance // this enables an owner to give allowance to multiple addresses mapping(address => mapping(address => uint256)) public allowance; // just added address public owner; constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; decimals = 18; owner = msg.sender; } function mint(address to, uint256 amount) public { require(msg.sender == owner, "only owner can create tokens"); totalSupply += amount; balanceOf[owner] += amount; } function transfer(address to, uint256 amount) public { require(balanceOf[msg.sender] >= amount, "you aint rich enough"); require(to != address(0), "cannot send to address(0)"); balanceOf[msg.sender] -= amount; balanceOf[to] += amount; } // just added function approve(address spender, uint256 amount) public { allowance[msg.sender][spender] = amount; } } |
1 |
allowance[msg.sender][spender] = amount; |
مقدار مجاز یا Allowance برای خرج کردن توکن ها تعیین میشود. در این دستور، msg.sender
صاحب اصلی توکن هاست و spender
آدرسی است که اجازه دریافت و خرج مقدار مشخصی از توکن های صاحب حساب را دریافت میکند. به عبارت دیگر، صاحب حساب (owner) به یک کاربر یا قرارداد دیگر (spender) اجازه میدهد تا به نیابت از او، مقداری توکن خرج کند.
اما تا اینجا فقط مقدار مجاز تعریف شده است. هنوز راهی برای استفاده از این اجازه نامه وجود ندارد. اینجا است که تابع مهم دیگری وارد عمل میشود: transferFrom
.
تحلیل عملکرد تابع transferFrom در سالیدیتی
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
contract ERC20 { string public name; string public symbol; mapping(address => uint256) public balanceOf; address public owner; uint8 public decimals; uint256 public totalSupply; // owner -> spender -> allowance // this enables an owner to give allowance to multiple addresses mapping(address => mapping(address => uint256)) public allowance; constructor(string memory _name, string memory _symbol) { name = _name; symbol = _symbol; decimals = 18; owner = msg.sender; } function mint(address to, uint256 amount) public { require(msg.sender == owner, "only owner can create tokens"); totalSupply += amount; balanceOf[owner] += amount; } function transfer(address to, uint256 amount) public { require(balanceOf[msg.sender] >= amount, "you aint rich enough"); require(to != address(0), "cannot send to address(0)"); balanceOf[msg.sender] -= amount; balanceOf[to] += amount; } function approve(address spender, uint256 amount) public { allowance[msg.sender][spender] = amount; } // just added function transferFrom(address from, address to, uint256 amount) public { require(balanceOf[from] >= amount, "not enough money"); if (msg.sender != from) { require(allowance[from][msg.sender] >= amount, "not enough allowance"); allowance[from][msg.sender] -= amount; } balanceOf[from] -= amount; balanceOf[to] += amount; } } |
ابتدا باید بدانیم که ممکن است خود صاحب توکن ها (owner) تابع transferFrom
را صدا بزند. در این حالت، بررسی allowance
ضرورتی ندارد، چرا که فردی که مجاز به خرج کردن توکن هاست همان دارنده اصلی آنهاست. بنابراین در این حالت مستقیماً موجودی ها (balances) را به روزرسانی میکنیم.
در غیر این صورت (یعنی اگر msg.sender
شخصی غیر از owner
باشد)، باید بررسی کنیم که آیا این شخص مجاز به خرج کردن مقدار موردنظر از حساب مالک است یا خیر. برای این کار:
-
ابتدا بررسی میکنیم که مقدار
allowance
کافی باشد. -
سپس مقدار خرج شده را از
allowance
کم میکنیم تا از خرج بینهایت جلوگیری شود. -
در نهایت، انتقال را انجام میدهیم.
بر اساس مشخصات رسمی EIP-20، سه تابع approve
، transfer
و transferFrom
باید پس از اجرای موفقیت آمیز مقدار true
را بازگردانند. این بازگشت مقدار نشان دهنده موفقیت عملکرد تابع برای کاربران و قراردادهای دیگر است که ممکن است به این خروجی نیاز داشته باشند. پس بیایید این را اضافه کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
contract ERC20 { string public name; string public symbol; mapping(address => uint256) public balanceOf; address public owner; uint8 public decimals; uint256 public totalSupply; // owner -> spender -> allowance // this enables an owner to give allowance to multiple addresses mapping(address => mapping(address => uint256)) public allowance; constructor( string memory _name, string memory _symbol ) { name = _name; symbol = _symbol; decimals = 18; owner = msg.sender; } function mint( address to, uint256 amount ) public { require(msg.sender == owner, "only owner can create tokens"); totalSupply += amount; balanceOf[owner] += amount; } function transfer( address to, uint256 amount ) public returns (bool) { require(balanceOf[msg.sender] >= amount, "you aint rich enough"); require(to != address(0), "cannot send to address(0)"); balanceOf[msg.sender] -= amount; balanceOf[to] += amount; return true; } function approve( address spender, uint256 amount ) public returns (bool) { allowance[msg.sender][spender] = amount; return true; } function transferFrom( address from, address to, uint256 amount ) public returns (bool) { require(balanceOf[from] >= amount, "not enough money"); require(to != address(0), "cannot send to address(0)"); if (msg.sender != from) { require(allowance[from][msg.sender] >= amount, "not enough allowance"); allowance[from][msg.sender] -= amount; } balanceOf[from] -= amount; balanceOf[to] += amount; return true; } } |
در این بخش، نکته مهمی در طراحی قراردادهای هوشمند مطرح میشود: جلوگیری از تکرار کد در توابع transfer
و transferFrom
. همانطور که اشاره شد، این دو تابع بخشی از کدشان – یعنی به روزرسانی موجودی ها – مشابه یکدیگر است. این نوع تکرار در برنامه نویسی معمولاً نشانه ای برای نیاز به بازسازی (refactoring) کد است.
راهحل: استخراج تابع داخلی (internal function)
برای جلوگیری از تکرار، میتوانیم منطق انتقال موجودی را در یک تابع جداگانه قرار دهیم. اما نکته بسیار مهم این است که این تابع نباید public
باشد؛ در غیر این صورت، کاربران خارجی میتوانند با صدا زدن آن، توکن ها را جابهجا کرده و عملاً دزدی انجام دهند!
بنابراین، بهترین گزینه استفاده از تابع داخلی (internal) است. توابع داخلی فقط از درون خود قرارداد یا قراردادهای مشتقشده از آن قابل فراخوانی هستند و از بیرون قابل دسترسی نیستند.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
contract ERC20 { string public name; string public symbol; mapping(address => uint256) public balanceOf; address public owner; uint8 public decimals; uint256 public totalSupply; // owner -> spender -> allowance // this enables an owner to give allowance to multiple addresses mapping(address => mapping(address => uint256)) public allowance; constructor( string memory _name, string memory _symbol ) { name = _name; symbol = _symbol; decimals = 18; owner = msg.sender; } function mint( address to, uint256 amount ) public { require(msg.sender == owner, "only owner can create tokens"); totalSupply += amount; balanceOf[owner] += amount; } function transfer( address to, uint256 amount ) public returns (bool) { return helperTransfer(msg.sender, to, amount); } function approve( address spender, uint256 amount ) public returns (bool) { allowance[msg.sender][spender] = amount; return true; } function transferFrom( address from, address to, uint256 amount ) public returns (bool) { if (msg.sender != from) { require(allowance[from][msg.sender] >= amount, "not enough allowance"); allowance[from][msg.sender] -= amount; } return helperTransfer(from, to, amount); } function helperTransfer( address from, address to, uint256 amount ) internal returns (bool) { require(balanceOf[from] >= amount, "not enough money"); require(to != address(0), "cannot send to address(0)"); balanceOf[from] -= amount; balanceOf[to] += amount; return true; } } |
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۱۱ اردیبهشت ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++C
- ADO.NET
- Adobe Flash
- Ajax
- AngularJS
- apache
- ARM
- Asp.Net
- ASP.NET MVC
- AVR
- Bootstrap
- CCNA
- CCNP
- CMD
- CSS
- Dreameaver
- EntityFramework
- HTML
- IOS
- jquery
- Linq
- Mysql
- Oracle
- PHP
- PHPMyAdmin
- Rational Rose
- silver light
- SQL Server
- Stimulsoft Reports
- Telerik
- UML
- VB.NET&VB6
- WPF
- Xml
- آموزش های پروژه محور
- اتوکد
- الگوریتم تقریبی
- امنیت
- اندروید
- اندروید استودیو
- بک ترک
- بیسیک فور اندروید
- پایتون
- جاوا
- جاوا اسکریپت
- جوملا
- دلفی
- دوره آموزش Go
- دوره های رایگان پیشنهادی
- زامارین
- سئو
- ساخت CMS
- سی شارپ
- شبکه و مجازی سازی
- طراحی الگوریتم
- طراحی بازی
- طراحی وب
- فتوشاپ
- فریم ورک codeigniter
- فلاتر
- کانستراکت
- کریستال ریپورت
- لاراول
- معماری کامپیوتر
- مهندسی اینترنت
- هوش مصنوعی
- یونیتی
- کتاب های آموزشی
- Android
- ASP.NET
- AVR
- LINQ
- php
- Workflow
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس