جولة تفصيلية لعقد ERC-20
مقدمة
أحد الاستخدامات الشائعة ليثريان هو أن تقوم مجموعة بإنشاء رمز قابل للتداول، بمعنى آخر العملة الخاصة بهم. عادةً ما تتبع هذه الرموز معيارًا، ERC-20. يجعل هذا المعيار من الممكن كتابة أدوات، مثل مجمعات السيولة والمحافظ، والتي تعمل مع جميع رموز ERC-20. في هذا المقال سنحلل تطبيق أوبن زبلين سوليديتي ERC20 (opens in a new tab)، بالإضافة إلى تعريف الواجهة (opens in a new tab).
هذا كود مصدري مشروح. إذا كنت ترغب في تطبيق ERC-20، اقرأ هذا البرنامج التعليمي (opens in a new tab).
الواجهة
الغرض من معيار مثل ERC-20 هو السماح بتطبيقات رموز متعددة قابلة للتشغيل البيني عبر التطبيقات، مثل المحافظ ومنصات التبادل اللامركزية. لتحقيق ذلك، نقوم بإنشاء واجهة (opens in a new tab). يمكن لأي كود يحتاج إلى استخدام عقد الرمز استخدام نفس التعريفات في الواجهة ويكون متوافقًا مع جميع عقود الرموز التي تستخدمه، سواء كان محفظة مثل ميتاماسك، أو تطبيقًا لامركزيًا مثل etherscan.io، أو عقدًا مختلفًا مثل مجمع السيولة.
إذا كنت مبرمجًا ذا خبرة، فربما تتذكر رؤية بنيات مماثلة في جافا (opens in a new tab) أو حتى في ملفات ترويسة لغة C (opens in a new tab).
هذا هو تعريف واجهة ERC-20 (opens in a new tab) من أوبن زبلين. إنها ترجمة للمعيار القابل للقراءة البشرية (opens in a new tab) إلى كود سوليديتي. بالطبع، الواجهة نفسها لا تحدد كيفية فعل أي شيء. هذا مشروح في الكود المصدري للعقد أدناه.
1// SPDX-License-Identifier: MITمن المفترض أن تتضمن ملفات سوليديتي مُعرِّف ترخيص. يمكنك رؤية قائمة التراخيص هنا (opens in a new tab). إذا كنت بحاجة إلى ترخيص مختلف، فقط وضحه في التعليقات.
1pragma solidity >=0.6.0 <0.8.0;لا تزال لغة سوليديتي تتطور بسرعة، وقد لا تكون الإصدارات الجديدة متوافقة مع الكود القديم (انظر هنا (opens in a new tab)). لذلك، من الجيد تحديد ليس فقط الحد الأدنى لإصدار اللغة، ولكن أيضًا الحد الأقصى للإصدار، وهو أحدث إصدار اختبرت الكود به.
1/**2 * @dev واجهة معيار ERC20 كما هو محدد في EIP.3 */إن @dev في التعليق هو جزء من تنسيق نات سبيك (opens in a new tab)، ويُستخدم لإنتاج
التوثيق من الكود المصدري.
1interface IERC20 {وفقًا للعرف، تبدأ أسماء الواجهات بـ I.
1 /**2 * @dev تُرجع كمية الرموز الموجودة.3 */4 function totalSupply() external view returns (uint256);هذه الدالة external، مما يعني أنه لا يمكن استدعاؤها إلا من خارج العقد (opens in a new tab).
تُرجع إجمالي المعروض من الرموز في العقد. تُرجع هذه القيمة باستخدام النوع الأكثر شيوعًا في إيثريوم، وهو 256 بت غير مُوقَّعة (256 بت هو
حجم الكلمة الأصلي لـ EVM). هذه الدالة هي أيضًا view، مما يعني أنها لا تغير الحالة، لذا يمكن تنفيذها على عقدة واحدة بدلاً من أن
تُشغلها كل عقدة في البلوك تشين. هذا النوع من الدوال لا يُنشئ معاملة ولا يكلف غازًا.
ملاحظة: نظريًا، قد يبدو أن منشئ العقد يمكنه الغش عن طريق إرجاع إجمالي معروض أصغر من القيمة الحقيقية، مما يجعل كل رمز يبدو أكثر قيمة مما هو عليه في الواقع. ومع ذلك، فإن هذا الخوف يتجاهل الطبيعة الحقيقية للبلوك تشين. يمكن التحقق من كل ما يحدث على البلوك تشين من قبل كل عقدة. لتحقيق ذلك، يتوفر كود لغة الآلة والتخزين الخاص بكل عقد على كل عقدة. على الرغم من أنك لست مطالبًا بنشر كود سوليديتي لعقدك، فلن يأخذك أحد على محمل الجد ما لم تنشر الكود المصدري وإصدار سوليديتي الذي تم تجميعه به، حتى يمكن التحقق منه مقابل كود لغة الآلة الذي قدمته. على سبيل المثال، انظر هذا العقد (opens in a new tab).
1 /**2 * @dev تُرجع كمية الرموز التي يملكها `account`.3 */4 function balanceOf(address account) external view returns (uint256);كما يوحي الاسم، تُرجع balanceOf رصيد حساب ما. يتم تحديد حسابات إيثريوم في سوليديتي باستخدام نوع address، الذي يحمل 160 بت.
هي أيضًا external وview.
1 /**2 * @dev تنقل `amount` من الرموز من حساب المتصل إلى `recipient`.3 *4 * تُرجع قيمة منطقية تشير إلى نجاح العملية.5 *6 * تُصدر حدث {Transfer}.7 */8 function transfer(address recipient, uint256 amount) external returns (bool);تنقل دالة transfer الرموز من المتصل إلى عنوان مختلف. هذا يتضمن تغييرًا في الحالة، لذا فهي ليست view.
عندما يستدعي مستخدم هذه الدالة، فإنها تنشئ معاملة وتكلف غازًا. كما أنها تُصدر حدثًا، Transfer، لإبلاغ الجميع على
البلوك تشين بالحدث.
للدالة نوعان من المخرجات لنوعين مختلفين من المتصلين:
- المستخدمون الذين يستدعون الدالة مباشرة من واجهة المستخدم. عادةً ما يرسل المستخدم معاملة
ولا ينتظر استجابة، والتي قد تستغرق وقتًا غير محدد. يمكن للمستخدم أن يرى ما حدث
من خلال البحث عن إيصال المعاملة (الذي يتم تحديده بواسطة تجزئة (هاش) المعاملة) أو بالبحث عن
حدث
Transfer. - العقود الأخرى التي تستدعي الدالة كجزء من معاملة شاملة. تحصل تلك العقود على النتيجة فورًا، لأنها تعمل في نفس المعاملة، لذا يمكنها استخدام القيمة المرجعة للدالة.
يتم إنشاء نفس نوع المخرجات بواسطة الدوال الأخرى التي تغير حالة العقد.
تسمح المخصصات لحساب بإنفاق بعض الرموز التي تخص مالكًا مختلفًا. هذا مفيد، على سبيل المثال، للعقود التي تعمل كبائعين. لا يمكن للعقود مراقبة الأحداث، لذلك إذا قام مشترٍ بتحويل الرموز إلى عقد البائع مباشرة، فلن يعرف ذلك العقد أنه تم الدفع له. بدلاً من ذلك، يسمح المشتري لعقد البائع بإنفاق مبلغ معين، ويقوم البائع بتحويل هذا المبلغ. يتم ذلك من خلال دالة يستدعيها عقد البائع، بحيث يمكن لعقد البائع معرفة ما إذا كانت العملية ناجحة.
1 /**2 * @dev تُرجع العدد المتبقي من الرموز التي سيُسمح لـ `spender` بإنفاقها نيابة عن `owner` من خلال {transferFrom}. هذه القيمة3 * صفر افتراضيًا.4 *5 * تتغير هذه القيمة عند استدعاء {approve} أو {transferFrom}.6 */7 function allowance(address owner, address spender) external view returns (uint256);تسمح دالة allowance لأي شخص بالاستعلام لمعرفة المخصص الذي يسمح به عنوان
(owner) لعنوان آخر (spender) بإنفاقه.
1 /**2 * @dev تحدد `amount` كمخصص لـ `spender` على رموز المتصل.3 *4 * تُرجع قيمة منطقية تشير إلى نجاح العملية.5 *6 * هام: احذر من أن تغيير المخصص بهذه الطريقة يحمل خطر7 * أن يستخدم شخص ما المخصص القديم والجديد معًا بسبب ترتيب معاملات غير موفق. أحد الحلول الممكنة للتخفيف من حالة السباق8 * هذه هو تقليل مخصص المنفق إلى 0 أولاً وتعيين9 * القيمة المطلوبة بعد ذلك:10 * https://github.com/ethereum/EIPs/issues/20#issuecomment-26352472911 *12 * تُصدر حدث {Approval}.13 */14 function approve(address spender, uint256 amount) external returns (bool);تنشئ دالة approve مخصصًا. تأكد من قراءة الرسالة حول
كيفية إساءة استخدامه. في إيثريوم، يمكنك التحكم في ترتيب معاملاتك الخاصة،
لكنك لا تستطيع التحكم في الترتيب الذي سيتم به تنفيذ معاملات الآخرين،
إلا إذا لم تقدم معاملتك حتى ترى أن معاملة الطرف الآخر قد حدثت.
1 /**2 * @dev تنقل `amount` من الرموز من `sender` إلى `recipient` باستخدام3 * آلية المخصص. يتم بعد ذلك خصم `amount` من مخصص المتصل4 *.5 *6 * تُرجع قيمة منطقية تشير إلى نجاح العملية.7 *8 * تُصدر حدث {Transfer}.9 */10 function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);أخيرًا، تُستخدم transferFrom من قبل المنفق لإنفاق المخصص بالفعل.
1
2 /**3 * @dev يُصدر عندما يتم نقل رموز `value` من حساب (`from`) إلى4 * آخر (`to`).5 *6 * لاحظ أن `value` قد تكون صفرًا.7 */8 event Transfer(address indexed from, address indexed to, uint256 value);9
10 /**11 * @dev يُصدر عندما يتم تعيين مخصص `spender` لـ `owner` من خلال12 * استدعاء {approve}. `value` هو المخصص الجديد.13 */14 event Approval(address indexed owner, address indexed spender, uint256 value);15}تُصدر هذه الأحداث عندما تتغير حالة عقد ERC-20.
العقد الفعلي
هذا هو العقد الفعلي الذي يطبق معيار ERC-20، مأخوذ من هنا (opens in a new tab). لا يُقصد استخدامه كما هو، ولكن يمكنك الوراثة (opens in a new tab) منه لتوسيعه إلى شيء قابل للاستخدام.
1// SPDX-License-Identifier: MIT2pragma solidity >=0.6.0 <0.8.0;
عبارات الاستيراد
بالإضافة إلى تعريفات الواجهة أعلاه، يستورد تعريف العقد ملفين آخرين:
1
2import "../../GSN/Context.sol";3import "./IERC20.sol";4import "../../math/SafeMath.sol";GSN/Context.solهو التعريفات المطلوبة لاستخدام أوبن جي إس إن (opens in a new tab)، وهو نظام يسمح للمستخدمين الذين ليس لديهم إيثر باستخدام البلوك تشين. لاحظ أن هذا إصدار قديم، إذا كنت ترغب في التكامل مع أوبن جي إس إن، استخدم هذا البرنامج التعليمي (opens in a new tab).- مكتبة SafeMath (opens in a new tab)، التي تمنع تجاوزات/تدفقات حسابية لإصدارات سوليديتي <0.8.0. في سوليديتي ≥0.8.0، تعود العمليات الحسابية تلقائيًا عند التجاوز/التدفق، مما يجعل SafeMath غير ضروري. يستخدم هذا العقد SafeMath للتوافق مع الإصدارات الأقدم من المجمع.
يوضح هذا التعليق الغرض من العقد.
1/**2 * @dev تطبيق واجهة {IERC20}.3 *4 * هذا التطبيق لا يعتمد على طريقة إنشاء الرموز. هذا يعني5 * أنه يجب إضافة آلية توريد في عقد مشتق باستخدام {_mint}.6 * لآلية عامة، انظر {ERC20PresetMinterPauser}.7 *8 * نصيحة: للحصول على شرح مفصل، راجع دليلنا9 * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[كيفية10 * تطبيق آليات التوريد].11 *12 * لقد اتبعنا إرشادات OpenZeppelin العامة: تعود الدوال بدلاً13 * من إرجاع `false` عند الفشل. هذا السلوك تقليدي14 * ولا يتعارض مع توقعات تطبيقات ERC20.15 *16 * بالإضافة إلى ذلك، يتم إصدار حدث {Approval} عند استدعاء {transferFrom}.17 * يسمح هذا للتطبيقات بإعادة بناء المخصص لجميع الحسابات فقط18 * من خلال الاستماع إلى الأحداث المذكورة. قد لا تصدر التطبيقات الأخرى لـ EIP19 * هذه الأحداث، حيث إنها غير مطلوبة في المواصفات.20 *21 * أخيرًا، تمت إضافة الدالتين غير القياسيتين {decreaseAllowance} و {increaseAllowance}22 * للتخفيف من المشكلات المعروفة حول تعيين23 * المخصصات. انظر {IERC20-approve}.24 */25
تعريف العقد
1contract ERC20 is Context, IERC20 {يحدد هذا السطر الوراثة، في هذه الحالة من IERC20 أعلاه و Context، لـ أوبن جي إس إن.
1
2 using SafeMath for uint256;3
يربط هذا السطر مكتبة SafeMath بنوع uint256. يمكنك العثور على هذه المكتبة
هنا (opens in a new tab).
تعريفات المتغيرات
تحدد هذه التعريفات متغيرات حالة العقد. يتم الإعلان عن هذه المتغيرات بأنها خاصة، ولكن
هذا يعني فقط أن العقود الأخرى على البلوك تشين لا يمكنها قراءتها. لا توجد
أسرار على البلوك تشين، فالبرنامج الموجود على كل عقدة لديه حالة كل عقد
عند كل كتلة. وفقًا للعرف، تتم تسمية متغيرات الحالة _<شيء ما>.
المتغيران الأولان هما تعيينات (opens in a new tab)، مما يعني أنهما يتصرفان تقريبًا بنفس طريقة المصفوفات الترابطية (opens in a new tab)، باستثناء أن المفاتيح هي قيم رقمية. يتم تخصيص التخزين فقط للإدخالات التي لها قيم مختلفة عن القيمة الافتراضية (صفر).
1 mapping (address => uint256) private _balances;التعيين الأول، _balances، هو العناوين وأرصدتها الخاصة بهذا الرمز. للوصول
إلى الرصيد، استخدم هذا الصيغة: _balances[<address>].
1 mapping (address => mapping (address => uint256)) private _allowances;يخزن هذا المتغير، _allowances، المخصصات التي تم شرحها سابقًا. الفهرس الأول هو مالك
الرموز، والثاني هو العقد الذي لديه المخصص. للوصول إلى المبلغ الذي يمكن للعنوان A
إنفاقه من حساب العنوان B، استخدم _allowances[B][A].
1 uint256 private _totalSupply;كما يوحي الاسم، يتتبع هذا المتغير إجمالي المعروض من الرموز.
1 string private _name;2 string private _symbol;3 uint8 private _decimals;تُستخدم هذه المتغيرات الثلاثة لتحسين قابلية القراءة. المتغيران الأولان واضحان، لكن _decimals
ليس كذلك.
من ناحية، لا تحتوي إيثريوم على متغيرات ذات فاصلة عائمة أو كسرية. من ناحية أخرى، يحب البشر القدرة على تقسيم الرموز. أحد أسباب استقرار الناس على الذهب كعملة هو أنه كان من الصعب إجراء تغيير عندما أراد شخص ما شراء ما يعادل قيمة بطة من بقرة.
الحل هو تتبع الأعداد الصحيحة، ولكن بدلاً من الرمز الحقيقي، يتم حساب رمز كسري يكاد يكون عديم القيمة. في حالة الإيثر، يُطلق على الرمز الكسري اسم wei، و 10^18 wei تساوي واحد ETH. عند الكتابة، 10,000,000,000,000 wei تساوي تقريبًا سنتًا أمريكيًا أو أوروبيًا واحدًا.
تحتاج التطبيقات إلى معرفة كيفية عرض رصيد الرمز. إذا كان لدى مستخدم 3,141,000,000,000,000,000 wei، فهل هذا
3.14 ETH؟ 31.41 ETH؟ 3,141 ETH؟ في حالة الإيثر، تم تعريف 10^18 wei إلى ETH، ولكن بالنسبة
لرمزك، يمكنك تحديد قيمة مختلفة. إذا كان تقسيم الرمز غير منطقي، فيمكنك استخدام
قيمة _decimals صفرية. إذا كنت ترغب في استخدام نفس المعيار مثل ETH، فاستخدم القيمة 18.
المنشئ
1 /**2 * @dev يضبط القيم لـ {name} و {symbol}، ويبدأ {decimals} بقيمة3 * افتراضية 18.4 *5 * لتحديد قيمة مختلفة لـ {decimals}، استخدم {_setupDecimals}.6 *7 * كل هذه القيم الثلاث غير قابلة للتغيير: لا يمكن ضبطها إلا مرة واحدة أثناء8 * الإنشاء.9 */10 constructor (string memory name_, string memory symbol_) public {11 // في Solidity ≥0.7.0، `public` ضمني ويمكن حذفه.12
13 _name = name_;14 _symbol = symbol_;15 _decimals = 18;16 }يتم استدعاء المنشئ عند إنشاء العقد لأول مرة. وفقًا للعرف، تتم تسمية معلمات الدالة <شيء ما>_.
دوال واجهة المستخدم
1 /**2 * @dev تُرجع اسم الرمز.3 */4 function name() public view returns (string memory) {5 return _name;6 }7
8 /**9 * @dev تُرجع رمز الرمز، وعادة ما يكون نسخة أقصر من الاسم.10 *11 */12 function symbol() public view returns (string memory) {13 return _symbol;14 }15
16 /**17 * @dev تُرجع عدد الكسور العشرية المستخدمة للحصول على تمثيله للمستخدم.18 * على سبيل المثال، إذا كانت `decimals` تساوي `2`، يجب عرض رصيد `505` من الرموز19 * للمستخدم كـ `5,05` (`505 / 10 ** 2`).20 *21 * عادة ما تختار الرموز قيمة 18، محاكاة للعلاقة بين22 * الإيثر والـ wei. هذه هي القيمة التي يستخدمها {ERC20}، ما لم يتم استدعاء {_setupDecimals}23 *.24 *25 * ملاحظة: تُستخدم هذه المعلومات لأغراض _العرض_ فقط: فهي لا تؤثر26 * بأي شكل من الأشكال على أي من حسابات العقد، بما في ذلك27 * {IERC20-balanceOf} و {IERC20-transfer}.28 */29 function decimals() public view returns (uint8) {30 return _decimals;31 }تساعد هذه الدوال، name، وsymbol، وdecimals، واجهات المستخدم على معرفة عقدك حتى تتمكن من عرضه بشكل صحيح.
نوع الإرجاع هو string memory، مما يعني إرجاع سلسلة مخزنة في الذاكرة. يمكن تخزين المتغيرات، مثل
السلاسل، في ثلاثة مواقع:
| العمر الافتراضي | الوصول إلى العقد | تكلفة الغاز | |
|---|---|---|---|
| الذاكرة | استدعاء دالة | قراءة/كتابة | عشرات أو مئات (أعلى للمواقع الأعلى) |
| Calldata | استدعاء دالة | قراءة فقط | لا يمكن استخدامه كنوع إرجاع، فقط كنوع معلمة دالة |
| التخزين | حتى يتم تغييره | قراءة/كتابة | مرتفع (800 للقراءة، 20 ألف للكتابة) |
في هذه الحالة، memory هي الخيار الأفضل.
قراءة معلومات الرمز
هذه دوال توفر معلومات حول الرمز، إما إجمالي المعروض أو رصيد الحساب.
1 /**2 * @dev انظر {IERC20-totalSupply}.3 */4 function totalSupply() public view override returns (uint256) {5 return _totalSupply;6 }تعيد دالة totalSupply إجمالي المعروض من الرموز.
1 /**2 * @dev انظر {IERC20-balanceOf}.3 */4 function balanceOf(address account) public view override returns (uint256) {5 return _balances[account];6 }قراءة رصيد الحساب. لاحظ أنه يُسمح لأي شخص بالحصول على رصيد حساب أي شخص آخر. لا فائدة من محاولة إخفاء هذه المعلومات، لأنها متوفرة على كل عقدة على أي حال. لا توجد أسرار على البلوك تشين.
نقل الرموز
1 /**2 * @dev انظر {IERC20-transfer}.3 *4 * المتطلبات:5 *6 * - لا يمكن أن يكون `recipient` هو العنوان الصفري.7 * - يجب أن يكون لدى المتصل رصيد لا يقل عن `amount`.8 */9 function transfer(address recipient, uint256 amount) public virtual override returns (bool) {يتم استدعاء دالة transfer لنقل الرموز من حساب المرسل إلى حساب مختلف. لاحظ أنه
على الرغم من أنها تعيد قيمة منطقية، إلا أن هذه القيمة دائمًا صحيحة. إذا فشل النقل،
يعود العقد عن الاستدعاء.
1 _transfer(_msgSender(), recipient, amount);2 return true;3 }تقوم دالة _transfer بالعمل الفعلي. إنها دالة خاصة لا يمكن استدعاؤها إلا من قبل
دوال العقد الأخرى. وفقًا للعرف، تتم تسمية الدوال الخاصة _<شيء ما>، مثل متغيرات
الحالة.
عادة في سوليديتي نستخدم msg.sender لمرسل الرسالة. ومع ذلك، فإن هذا يعطل
أوبن جي إس إن (opens in a new tab). إذا أردنا السماح بالمعاملات بدون إيثر باستخدام رمزنا، فنحن
بحاجة إلى استخدام _msgSender(). تعيد msg.sender للمعاملات العادية، ولكن بالنسبة للمعاملات بدون إيثر،
تعيد الموقع الأصلي وليس العقد الذي نقل الرسالة.
دوال المخصصات
هذه هي الدوال التي تطبق وظيفة المخصصات: allowance، وapprove، وtransferFrom،
و_approve. بالإضافة إلى ذلك، يتجاوز تطبيق أوبن زبلين المعيار الأساسي ليشمل بعض الميزات التي تحسن
الأمان: increaseAllowance، وdecreaseAllowance.
دالة المخصص
1 /**2 * @dev انظر {IERC20-allowance}.3 */4 function allowance(address owner, address spender) public view virtual override returns (uint256) {5 return _allowances[owner][spender];6 }تسمح دالة allowance للجميع بالتحقق من أي مخصص.
دالة الموافقة
1 /**2 * @dev انظر {IERC20-approve}.3 *4 * المتطلبات:5 *6 * - لا يمكن أن يكون `spender` هو العنوان الصفري.7 */8 function approve(address spender, uint256 amount) public virtual override returns (bool) {يتم استدعاء هذه الدالة لإنشاء مخصص. إنها مشابهة لدالة transfer أعلاه:
- تستدعي الدالة فقط دالة داخلية (في هذه الحالة،
_approve) تقوم بالعمل الحقيقي. - تعيد الدالة إما
صحيح(إذا نجحت) أو تعود (إذا لم تنجح).
1 _approve(_msgSender(), spender, amount);2 return true;3 }نستخدم الدوال الداخلية لتقليل عدد الأماكن التي تحدث فيها تغييرات في الحالة. أي دالة تغير الحالة هي خطر أمني محتمل يحتاج إلى تدقيق للأمان. بهذه الطريقة لدينا فرص أقل للخطأ.
دالة transferFrom
هذه هي الدالة التي يستدعيها المنفق لإنفاق مخصص. يتطلب هذا عمليتين: نقل المبلغ الذي يتم إنفاقه وتقليل المخصص بهذا المبلغ.
1 /**2 * @dev انظر {IERC20-transferFrom}.3 *4 * يصدر حدث {Approval} يشير إلى المخصص المحدث. هذا غير5 * مطلوب من قبل EIP. انظر الملاحظة في بداية {ERC20}.6 *7 * المتطلبات:8 *9 * - لا يمكن أن يكون `sender` و `recipient` هما العنوان الصفري.10 * - يجب أن يكون لدى `sender` رصيد لا يقل عن `amount`.11 * - يجب أن يكون لدى المتصل مخصص لرموز `sender` لا يقل عن12 * `amount`.13 */14 function transferFrom(address sender, address recipient, uint256 amount) public virtual15 override returns (bool) {16 _transfer(sender, recipient, amount);
يقوم استدعاء الدالة a.sub(b, "message") بشيئين. أولاً، يحسب a-b، وهو المخصص الجديد.
ثانيًا، يتحقق من أن هذه النتيجة ليست سلبية. إذا كانت سلبية، يعود الاستدعاء مع الرسالة المقدمة. لاحظ أنه عندما يعود الاستدعاء، يتم تجاهل أي معالجة تم إجراؤها مسبقًا أثناء هذا الاستدعاء، لذلك لا نحتاج إلى
التراجع عن _transfer.
1 _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,2 "ERC20: transfer amount exceeds allowance"));3 return true;4 }إضافات السلامة من أوبن زبلين
من الخطورة تعيين مخصص غير صفري إلى قيمة أخرى غير صفرية، لأنك تتحكم فقط في ترتيب معاملاتك الخاصة، وليس معاملات أي شخص آخر. تخيل أن لديك مستخدمين، أليس التي هي ساذجة وبيل الذي هو غير أمين. تريد أليس بعض الخدمات من بيل، والتي تعتقد أنها تكلف خمسة رموز - لذلك تمنح بيل مخصصًا بخمسة رموز.
ثم يتغير شيء ما ويرتفع سعر بيل إلى عشرة رموز. أليس، التي لا تزال تريد الخدمة، ترسل معاملة تحدد مخصص بيل بعشرة. في اللحظة التي يرى فيها بيل هذه المعاملة الجديدة في مجمع المعاملات، يرسل معاملة تنفق رموز أليس الخمسة ولها سعر غاز أعلى بكثير بحيث يتم تعدينها بشكل أسرع. بهذه الطريقة يمكن لبيل أن ينفق أولاً خمسة رموز ثم، بمجرد تعدين مخصص أليس الجديد، ينفق عشرة أخرى بسعر إجمالي قدره خمسة عشر رمزًا، أي أكثر مما كانت أليس تنوي تفويضه. تسمى هذه التقنية التشغيل الأمامي (opens in a new tab)
| معاملة أليس | نونس أليس | معاملة بيل | نونس بيل | مخصص بيل | إجمالي دخل بيل من أليس |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
| approve(Bill, 10) | 11 | 10 | 5 | ||
| transferFrom(Alice, Bill, 10) | 10,124 | 0 | 15 |
لتجنب هذه المشكلة، تسمح لك هاتان الدالتان (increaseAllowance و decreaseAllowance)
بتعديل المخصص بمقدار معين. لذا، إذا كان بيل قد أنفق بالفعل خمسة رموز، فسيتمكن فقط
من إنفاق خمسة أخرى. اعتمادًا على التوقيت، هناك طريقتان يمكن أن يعمل بهما هذا، وكلاهما
ينتهي بحصول بيل على عشرة رموز فقط:
أ:
| معاملة أليس | نونس أليس | معاملة بيل | نونس بيل | مخصص بيل | إجمالي دخل بيل من أليس |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | ||
| increaseAllowance(Bill, 5) | 11 | 0+5 = 5 | 5 | ||
| transferFrom(Alice, Bill, 5) | 10,124 | 0 | 10 |
ب:
| معاملة أليس | نونس أليس | معاملة بيل | نونس بيل | مخصص بيل | إجمالي دخل بيل من أليس |
|---|---|---|---|---|---|
| approve(Bill, 5) | 10 | 5 | 0 | ||
| increaseAllowance(Bill, 5) | 11 | 5+5 = 10 | 0 | ||
| transferFrom(Alice, Bill, 10) | 10,124 | 0 | 10 |
1 /**2 * @dev يزيد ذريًا المخصص الممنوح لـ `spender` من قبل المتصل.3 *4 * هذا بديل لـ {approve} يمكن استخدامه للتخفيف من5 * المشكلات الموصوفة في {IERC20-approve}.6 *7 * يصدر حدث {Approval} يشير إلى المخصص المحدث.8 *9 * المتطلبات:10 *11 * - لا يمكن أن يكون `spender` هو العنوان الصفري.12 */13 function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {14 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue));15 return true;16 }الدالة a.add(b) هي إضافة آمنة. في الحالة غير المحتملة التي يكون فيها a+b>=2^256، لا تلتف
حول الطريقة التي تعمل بها الإضافة العادية.
1
2 /**3 * @dev يقلل ذريًا المخصص الممنوح لـ `spender` من قبل المتصل.4 *5 * هذا بديل لـ {approve} يمكن استخدامه للتخفيف من6 * المشكلات الموصوفة في {IERC20-approve}.7 *8 * يصدر حدث {Approval} يشير إلى المخصص المحدث.9 *10 * المتطلبات:11 *12 * - لا يمكن أن يكون `spender` هو العنوان الصفري.13 * - يجب أن يكون لدى `spender` مخصص للمتصل لا يقل عن14 * `subtractedValue`.15 */16 function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {17 _approve(_msgSender(), spender, _allowances[_msgSender()][spender].sub(subtractedValue,18 "ERC20: decreased allowance below zero"));19 return true;20 }الدوال التي تعدل معلومات الرمز
هذه هي الدوال الأربع التي تقوم بالعمل الفعلي: _transfer، و_mint، و_burn، و_approve.
الدالة _transfer
1 /**2 * @dev تنقل رموز `amount` من `sender` إلى `recipient`.3 *4 * هذه الدالة الداخلية تعادل {transfer}، ويمكن استخدامها5 * على سبيل المثال، لتنفيذ رسوم الرموز التلقائية، وآليات الشطب، إلخ.6 *7 * تصدر حدث {Transfer}.8 *9 * المتطلبات:10 *11 * - لا يمكن أن يكون `sender` هو العنوان الصفري.12 * - لا يمكن أن يكون `recipient` هو العنوان الصفري.13 * - يجب أن يكون لدى `sender` رصيد لا يقل عن `amount`.14 */15 function _transfer(address sender, address recipient, uint256 amount) internal virtual {هذه الدالة، _transfer، تنقل الرموز من حساب إلى آخر. يتم استدعاؤها من قبل كل من
transfer (للتحويلات من حساب المرسل الخاص) وtransferFrom (لاستخدام المخصصات
للتحويل من حساب شخص آخر).
1 require(sender != address(0), "ERC20: transfer from the zero address");2 require(recipient != address(0), "ERC20: transfer to the zero address");لا أحد يمتلك فعليًا العنوان الصفري في إيثريوم (أي لا أحد يعرف مفتاحًا خاصًا يتم تحويل المفتاح العام المطابق له إلى العنوان الصفري). عندما يستخدم الناس هذا العنوان، فإنه عادة ما يكون خطأ برمجيًا - لذلك نفشل إذا تم استخدام العنوان الصفري كمرسل أو مستلم.
1 _beforeTokenTransfer(sender, recipient, amount);2
هناك طريقتان لاستخدام هذا العقد:
- استخدمه كقالب للكود الخاص بك
- الوراثة منه (opens in a new tab)، وتجاوز تلك الدوال التي تحتاج إلى تعديلها فقط
الطريقة الثانية أفضل بكثير لأن كود أوبن زبلين ERC-20 قد تم تدقيقه بالفعل وتبين أنه آمن. عندما تستخدم الوراثة، يكون من الواضح ما هي الدوال التي تعدلها، ولكي يثق الناس في عقدك، يحتاجون فقط إلى تدقيق تلك الدوال المحددة.
غالبًا ما يكون من المفيد أداء دالة في كل مرة تنتقل فيها الرموز. ومع ذلك، فإن _transfer هي دالة مهمة جدًا ومن
الممكن كتابتها بشكل غير آمن (انظر أدناه)، لذلك من الأفضل عدم تجاوزها. الحل هو _beforeTokenTransfer، وهي
دالة ربط (hook) (opens in a new tab). يمكنك تجاوز هذه الدالة، وسيتم استدعاؤها في كل عملية نقل.
1 _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");2 _balances[recipient] = _balances[recipient].add(amount);هذه هي الأسطر التي تقوم بالفعل بالنقل. لاحظ أنه لا يوجد شيء بينهما، وأننا نطرح المبلغ المنقول من المرسل قبل إضافته إلى المستلم. هذا مهم لأنه إذا كان هناك استدعاء لعقد مختلف في المنتصف، فقد يتم استخدامه لخداع هذا العقد. بهذه الطريقة يكون النقل ذريًا، لا يمكن أن يحدث شيء في منتصفه.
1 emit Transfer(sender, recipient, amount);2 }أخيرًا، أصدر حدث Transfer. لا يمكن الوصول إلى الأحداث من قبل العقود الذكية، ولكن الكود الذي يعمل خارج البلوك تشين
يمكنه الاستماع إلى الأحداث والتفاعل معها. على سبيل المثال، يمكن للمحفظة تتبع متى يحصل المالك على المزيد من الرموز.
الدالتان _mint و _burn
هاتان الدالتان (_mint و _burn) تعدلان إجمالي المعروض من الرموز.
إنهما داخليتان ولا توجد دالة تستدعيهما في هذا العقد،
لذا فهما مفيدتان فقط إذا ورثت من العقد وأضفت منطقك الخاص
لتحديد الظروف التي يتم فيها سك رموز جديدة أو حرق الرموز
الحالية.
ملاحظة: لكل رمز ERC-20 منطقه التجاري الخاص الذي يملي إدارة الرمز.
على سبيل المثال، قد يستدعي عقد ذو معروض ثابت _mint فقط
في المنشئ ولا يستدعي _burn أبدًا. العقد الذي يبيع الرموز
سيستدعي _mint عند الدفع له، ومن المفترض أن يستدعي _burn في مرحلة ما
لتجنب التضخم الجامح.
1 /** @dev تنشئ `amount` من الرموز وتخصصها لـ `account`، مما يزيد2 * من إجمالي المعروض.3 *4 * تصدر حدث {Transfer} مع تعيين `from` إلى العنوان الصفري.5 *6 * المتطلبات:7 *8 * - لا يمكن أن يكون `to` هو العنوان الصفري.9 */10 function _mint(address account, uint256 amount) internal virtual {11 require(account != address(0), "ERC20: mint to the zero address");12 _beforeTokenTransfer(address(0), account, amount);13 _totalSupply = _totalSupply.add(amount);14 _balances[account] = _balances[account].add(amount);15 emit Transfer(address(0), account, amount);16 }تأكد من تحديث _totalSupply عند تغير العدد الإجمالي للرموز.
1 /**2 * @dev تدمر `amount` من الرموز من `account`، مما يقلل من3 * إجمالي المعروض.4 *5 * تصدر حدث {Transfer} مع تعيين `to` إلى العنوان الصفري.6 *7 * المتطلبات:8 *9 * - لا يمكن أن يكون `account` هو العنوان الصفري.10 * - يجب أن يكون لدى `account` على الأقل `amount` من الرموز.11 */12 function _burn(address account, uint256 amount) internal virtual {13 require(account != address(0), "ERC20: burn from the zero address");14
15 _beforeTokenTransfer(account, address(0), amount);16
17 _balances[account] = _balances[account].sub(amount, "ERC20: burn amount exceeds balance");18 _totalSupply = _totalSupply.sub(amount);19 emit Transfer(account, address(0), amount);20 }دالة _burn متطابقة تقريبًا مع _mint، إلا أنها تعمل في الاتجاه الآخر.
الدالة _approve
هذه هي الدالة التي تحدد المخصصات بالفعل. لاحظ أنها تسمح للمالك بتحديد مخصص أعلى من رصيد المالك الحالي. هذا مقبول لأن الرصيد يتم التحقق منه في وقت التحويل، حيث يمكن أن يكون مختلفًا عن الرصيد عند إنشاء المخصص.
1 /**2 * @dev تحدد `amount` كمخصص لـ `spender` على رموز `owner`.3 *4 * هذه الدالة الداخلية تعادل `approve`، ويمكن استخدامها5 * على سبيل المثال، لتعيين مخصصات تلقائية لبعض النظم الفرعية، إلخ.6 *7 * تصدر حدث {Approval}.8 *9 * المتطلبات:10 *11 * - لا يمكن أن يكون `owner` هو العنوان الصفري.12 * - لا يمكن أن يكون `spender` هو العنوان الصفري.13 */14 function _approve(address owner, address spender, uint256 amount) internal virtual {15 require(owner != address(0), "ERC20: approve from the zero address");16 require(spender != address(0), "ERC20: approve to the zero address");17
18 _allowances[owner][spender] = amount;
أصدر حدث Approval. اعتمادًا على كيفية كتابة التطبيق، يمكن إبلاغ عقد المنفق بالموافقة
إما من قبل المالك أو من قبل خادم يستمع إلى هذه الأحداث.
1 emit Approval(owner, spender, amount);2 }3
تعديل متغير الكسور العشرية
1
2 /**3 * @dev يضبط {decimals} على قيمة أخرى غير القيمة الافتراضية 18.4 *5 * تحذير: يجب استدعاء هذه الدالة فقط من المنشئ. معظم6 * التطبيقات التي تتفاعل مع عقود الرموز لن تتوقع7 * أن يتغير {decimals} أبدًا، وقد تعمل بشكل غير صحيح إذا حدث ذلك.8 */9 function _setupDecimals(uint8 decimals_) internal {10 _decimals = decimals_;11 }تعدل هذه الدالة متغير _decimals الذي يستخدم لإخبار واجهات المستخدم بكيفية تفسير المبلغ.
يجب أن تستدعيها من المنشئ. سيكون من غير الأمين استدعاؤها في أي وقت لاحق، والتطبيقات
غير مصممة للتعامل مع ذلك.
خطافات
1
2 /**3 * @dev خطاف يتم استدعاؤه قبل أي نقل للرموز. وهذا يشمل4 * السك والحرق.5 *6 * شروط الاستدعاء:7 *8 * - عندما يكون `from` و `to` كلاهما غير صفريين، سيتم نقل `amount` من رموز `from`9 * إلى `to`.10 * - عندما يكون `from` صفرًا، سيتم سك `amount` من الرموز لـ `to`.11 * - عندما يكون `to` صفرًا، سيتم حرق `amount` من رموز `from`.12 * - لا يكون `from` و `to` كلاهما صفرًا أبدًا.13 *14 * لمعرفة المزيد عن الخطافات، انتقل إلى xref:ROOT:extending-contracts.adoc#using-hooks[استخدام الخطافات].15 */16 function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual { }17}هذه هي دالة الخطاف التي سيتم استدعاؤها أثناء عمليات النقل. إنها فارغة هنا، ولكن إذا كنت بحاجة إليها للقيام بشيء ما، فما عليك سوى تجاوزها.
الخلاصة
للمراجعة، إليك بعض أهم الأفكار في هذا العقد (في رأيي، من المحتمل أن يختلف رأيك):
- There are no secrets on the blockchain. أي معلومات يمكن لعقد ذكي الوصول إليها متاحة للعالم بأسره.
- يمكنك التحكم في ترتيب معاملاتك الخاصة، ولكن ليس متى تحدث معاملات الآخرين. هذا هو السبب في أن تغيير المخصص يمكن أن يكون خطيرًا، لأنه يتيح للمنفق إنفاق مجموع المخصصين.
- القيم من نوع
uint256تلتف. بمعنى آخر، 0-1=2^256-1. إذا لم يكن هذا هو السلوك المرغوب، فيجب عليك التحقق منه (أو استخدام مكتبة SafeMath التي تقوم بذلك نيابة عنك). لاحظ أن هذا تغير في سوليديتي 0.8.0 (opens in a new tab). - قم بجميع تغييرات الحالة من نوع معين في مكان معين، لأن ذلك يجعل التدقيق أسهل.
هذا هو السبب في أن لدينا، على سبيل المثال،
_approve، الذي يتم استدعاؤه من قبلapprove، وtransferFrom، وincreaseAllowance، وdecreaseAllowance - يجب أن تكون تغييرات الحالة ذرية، دون أي إجراء آخر في منتصفها (كما يمكنك أن ترى
في
_transfer). هذا لأنه أثناء تغيير الحالة لديك حالة غير متسقة. على سبيل المثال، بين وقت الخصم من رصيد المرسل ووقت الإضافة إلى رصيد المستلم، يوجد عدد أقل من الرموز مما ينبغي. يمكن أن يساء استخدام هذا إذا كانت هناك عمليات بينهما، خاصة استدعاءات لعقد مختلف.
الآن بعد أن رأيت كيف تمت كتابة عقد أوبن زبلين ERC-20، وخاصة كيف تم جعله أكثر أمانًا، اذهب واكتب عقودك وتطبيقاتك الآمنة.
انظر هنا لمزيد من أعمالي (opens in a new tab).
آخر تحديث للصفحة: 3 مارس 2026
