تخطي إلى المحتوى الرئيسي

جولة تفصيلية في عقد ⁦ERC-20⁩

Solidity
erc-20
مبتدئ
أوري بوميرانتس
9 مارس 2021
25 دقيقة للقراءة

مقدمة

أحد أكثر الاستخدامات شيوعًا لشبكة إيثيريوم هو قيام مجموعة بإنشاء رمز مميز قابل للتداول، وهو ما يمثل عملتهم الخاصة بمعنى ما. تتبع هذه الرموز المميزة عادةً معيارًا قياسيًا، وهو ERC-20. يجعل هذا المعيار من الممكن كتابة أدوات، مثل مجمعات السيولة والمحافظ، تعمل مع جميع رموز ERC-20 المميزة. في هذه المقالة، سنقوم بتحليل تنفيذ ⁦ERC20⁩ بلغة Solidity من أوبن زبلن (opens in a new tab)، بالإضافة إلى تعريف الواجهة (opens in a new tab).

هذا كود مصدري مشروح. إذا كنت ترغب في تنفيذ ERC-20، اقرأ هذا البرنامج التعليمي (opens in a new tab).

الواجهة

الغرض من معيار مثل ERC-20 هو السماح بالعديد من تطبيقات الرموز المميزة القابلة للتشغيل البيني عبر التطبيقات، مثل المحافظ ومنصات التداول اللامركزية. لتحقيق ذلك، نقوم بإنشاء واجهة (opens in a new tab). يمكن لأي كود يحتاج إلى استخدام عقد الرمز المميز استخدام نفس التعريفات في الواجهة ليكون متوافقًا مع جميع عقود الرموز المميزة التي تستخدمها، سواء كانت محفظة مثل ميتاماسك، أو تطبيق لامركزي (dapp) مثل Etherscan، أو عقدًا مختلفًا مثل مجمع سيولة.

Illustration of the ERC-20 interface

إذا كنت مبرمجًا متمرسًا، فربما تتذكر رؤية بنيات مشابهة في Java (opens in a new tab) أو حتى في ملفات ترويسة C (opens in a new tab).

هذا تعريف لـ واجهة ⁦ERC-20 (opens in a new tab) من أوبن زبلن. إنها ترجمة لـ المعيار القابل للقراءة من قبل البشر (opens in a new tab) إلى كود Solidity. بالطبع، الواجهة نفسها لا تحدد كيفية القيام بأي شيء. يتم شرح ذلك في الكود المصدري للعقد أدناه.

 

// SPDX-License-Identifier: MIT

من المفترض أن تتضمن ملفات Solidity مُعرّف ترخيص. يمكنك رؤية قائمة التراخيص هنا (opens in a new tab). إذا كنت بحاجة إلى ترخيص مختلف، فما عليك سوى شرح ذلك في التعليقات.

 

pragma solidity >=0.6.0 <0.8.0;

لا تزال لغة Solidity تتطور بسرعة، وقد لا تكون الإصدارات الجديدة متوافقة مع الكود القديم (انظر هنا (opens in a new tab)). لذلك، من الجيد تحديد ليس فقط الحد الأدنى لإصدار اللغة، ولكن أيضًا الحد الأقصى للإصدار، وهو أحدث إصدار قمت باختبار الكود به.

 

/**
 * @dev واجهة معيار ERC-20 كما هو محدد في EIP.
 */

يعد @dev في التعليق جزءًا من تنسيق NatSpec (opens in a new tab)، والذي يُستخدم لإنتاج الوثائق من الكود المصدري.

 

interface IERC20 {

حسب العرف، تبدأ أسماء الواجهات بـ I.

 

    /**
     * @dev يُرجع كمية الرموز المميزة الموجودة.
     */
    function totalSupply() external view returns (uint256);

هذه الدالة هي external، مما يعني أنه لا يمكن استدعاؤها إلا من خارج العقد (opens in a new tab). إنها تُرجع إجمالي المعروض من الرموز المميزة في العقد. يتم إرجاع هذه القيمة باستخدام النوع الأكثر شيوعًا في إيثيريوم، وهو unsigned 256 bits (256 bits هو حجم الكلمة الأصلي لآلة إيثيريوم الافتراضية). هذه الدالة هي أيضًا view، مما يعني أنها لا تغير الحالة، لذلك يمكن تنفيذها على عقدة واحدة بدلاً من جعل كل عقدة في سلسلة الكتل تقوم بتشغيلها. هذا النوع من الدوال لا يُنشئ معاملة ولا يكلف غاز.

ملاحظة: من الناحية النظرية، قد يبدو أن منشئ العقد يمكنه الغش عن طريق إرجاع إجمالي معروض أصغر من القيمة الحقيقية، مما يجعل كل رمز مميز يبدو أكثر قيمة مما هو عليه في الواقع. ومع ذلك، يتجاهل هذا الخوف الطبيعة الحقيقية لسلسلة الكتل. يمكن التحقق من كل ما يحدث على سلسلة الكتل بواسطة كل عقدة. لتحقيق ذلك، يتوفر كود لغة الآلة والتخزين الخاص بكل عقد على كل عقدة. على الرغم من أنك لست مطالبًا بنشر كود Solidity الخاص بعقدك، فلن يأخذك أحد على محمل الجد ما لم تنشر الكود المصدري وإصدار Solidity الذي تم تجميعه به، بحيث يمكن التحقق منه مقابل كود لغة الآلة الذي قدمته. على سبيل المثال، انظر هذا العقد (opens in a new tab).

 

    /**
     * @dev يُرجع كمية الرموز المميزة المملوكة لـ `account`.
     */
    function balanceOf(address account) external view returns (uint256);

كما يوحي الاسم، تُرجع balanceOf رصيد حساب. يتم تحديد حسابات إيثيريوم في Solidity باستخدام النوع address، والذي يحمل 160 bits. وهي أيضًا external و view.

 

    /**
     * @dev ينقل `amount` من الرموز المميزة من حساب المتصل إلى `recipient`.
     *
     * يُرجع قيمة منطقية تشير إلى ما إذا كانت العملية قد نجحت.
     *
     * يُصدر حدث {Transfer}.
     */
    function transfer(address recipient, uint256 amount) external returns (bool);

تقوم الدالة transfer بتحويل الرموز المميزة من المستدعي إلى عنوان مختلف. يتضمن هذا تغييرًا في الحالة، لذلك فهي ليست view. عندما يستدعي المستخدم هذه الدالة، فإنها تُنشئ معاملة وتكلف غازًا. كما أنها تُصدر حدثًا، Transfer، لإبلاغ الجميع على سلسلة الكتل بالحدث.

تحتوي الدالة على نوعين من المخرجات لنوعين مختلفين من المستدعين:

  • المستخدمون الذين يستدعون الدالة مباشرة من واجهة المستخدم. عادةً ما يرسل المستخدم معاملة ولا ينتظر استجابة، والتي قد تستغرق وقتًا غير محدد. يمكن للمستخدم رؤية ما حدث من خلال البحث عن إيصال المعاملة (والذي يتم تحديده بواسطة تجزئة المعاملة) أو من خلال البحث عن الحدث Transfer.
  • العقود الأخرى، التي تستدعي الدالة كجزء من معاملة شاملة. تحصل هذه العقود على النتيجة على الفور، لأنها تعمل في نفس المعاملة، لذلك يمكنها استخدام القيمة المرجعة للدالة.

يتم إنشاء نفس النوع من المخرجات بواسطة الدوال الأخرى التي تغير حالة العقد.

 

تسمح السماحيات لحساب بإنفاق بعض الرموز المميزة التي تنتمي إلى مالك مختلف. هذا مفيد، على سبيل المثال، للعقود التي تعمل كبائعين. لا يمكن للعقود مراقبة الأحداث، لذلك إذا قام المشتري بتحويل الرموز المميزة إلى عقد البائع مباشرة، فلن يعرف هذا العقد أنه تم الدفع له. بدلاً من ذلك، يسمح المشتري لعقد البائع بإنفاق مبلغ معين، ويقوم البائع بتحويل هذا المبلغ. يتم ذلك من خلال دالة يستدعيها عقد البائع، حتى يتمكن عقد البائع من معرفة ما إذا كان ناجحًا.

    /**
     * @dev يُرجع العدد المتبقي من الرموز المميزة التي سيُسمح لـ `spender` بإنفاقها نيابة عن `owner` من خلال {transferFrom}. هذه القيمة صفر افتراضيًا.
     *
     * تتغير هذه القيمة عند استدعاء {approve} أو {transferFrom}.
     */
    function allowance(address owner, address spender) external view returns (uint256);

تتيح الدالة allowance لأي شخص الاستعلام لمعرفة ما هي السماحية التي يتيحها عنوان واحد (owner) لعنوان آخر (spender) لإنفاقها.

 

تُنشئ الدالة approve سماحية. تأكد من قراءة الرسالة حول كيفية إساءة استخدامها. في إيثيريوم، أنت تتحكم في ترتيب معاملاتك الخاصة، ولكن لا يمكنك التحكم في الترتيب الذي سيتم به تنفيذ معاملات الأشخاص الآخرين، ما لم تقم بإرسال معاملتك الخاصة حتى ترى أن معاملة الطرف الآخر قد حدثت.

 

أخيرًا، يتم استخدام transferFrom بواسطة المنفق لإنفاق السماحية فعليًا.

 

يتم إصدار هذه الأحداث عندما تتغير حالة عقد ERC-20.

العقد الفعلي

هذا هو العقد الفعلي الذي ينفذ معيار ERC-20، مأخوذ من هنا (opens in a new tab). ليس المقصود استخدامه كما هو، ولكن يمكنك الوراثة (opens in a new tab) منه لتوسيعه إلى شيء قابل للاستخدام.

// SPDX-License-Identifier: MIT
pragma solidity >=0.6.0 <0.8.0;

 

عبارات الاستيراد

بالإضافة إلى تعريفات الواجهة أعلاه، يستورد تعريف العقد ملفين آخرين:


import "../../GSN/Context.sol";
import "./IERC20.sol";
import "../../math/SafeMath.sol";
  • GSN/Context.sol هي التعريفات المطلوبة لاستخدام OpenGSN (opens in a new tab)، وهو نظام يسمح للمستخدمين الذين ليس لديهم إيثر باستخدام سلسلة الكتل. لاحظ أن هذا إصدار قديم، إذا كنت ترغب في التكامل مع OpenGSN استخدم هذا البرنامج التعليمي (opens in a new tab).
  • مكتبة SafeMath (opens in a new tab)، والتي تمنع تجاوز السعة الحسابي (overflows/underflows) لإصدارات Solidity <0.8.0. في Solidity ≥0.8.0، تتراجع العمليات الحسابية تلقائيًا عند تجاوز السعة، مما يجعل SafeMath غير ضرورية. يستخدم هذا العقد SafeMath للتوافق مع الإصدارات السابقة من المترجم.

 

يشرح هذا التعليق الغرض من العقد.

تعريف العقد

contract ERC20 is Context, IERC20 {

يحدد هذا السطر الوراثة، في هذه الحالة من IERC20 من الأعلى و Context، لـ OpenGSN.

 


    using SafeMath for uint256;

يربط هذا السطر مكتبة SafeMath بالنوع uint256. يمكنك العثور على هذه المكتبة هنا (opens in a new tab).

تعريفات المتغيرات

تحدد هذه التعريفات متغيرات حالة العقد. يتم الإعلان عن هذه المتغيرات كـ private، ولكن هذا يعني فقط أن العقود الأخرى على سلسلة الكتل لا يمكنها قراءتها. لا توجد أسرار على سلسلة الكتل، فالبرنامج الموجود على كل عقدة يحتوي على حالة كل عقد في كل كتلة. حسب العرف، تتم تسمية متغيرات الحالة بـ _<something>.

أول متغيرين هما تعيينات (mappings) (opens in a new tab)، مما يعني أنهما يتصرفان تقريبًا مثل المصفوفات الترابطية (opens in a new tab)، باستثناء أن المفاتيح عبارة عن قيم رقمية. يتم تخصيص التخزين فقط للإدخالات التي لها قيم مختلفة عن القيمة الافتراضية (الصفر).

    mapping (address => uint256) private _balances;

التعيين الأول، _balances، هو العناوين وأرصدتها الخاصة من هذا الرمز المميز. للوصول إلى الرصيد، استخدم هذه الصيغة: _balances[<address>].

 

    mapping (address => mapping (address => uint256)) private _allowances;

يخزن هذا المتغير، _allowances، السماحيات الموضحة سابقًا. المؤشر الأول هو مالك الرموز المميزة، والثاني هو العقد الذي لديه السماحية. للوصول إلى المبلغ الذي يمكن للعنوان A إنفاقه من حساب العنوان B، استخدم _allowances[B][A].

 

    uint256 private _totalSupply;

كما يوحي الاسم، يتتبع هذا المتغير إجمالي المعروض من الرموز المميزة.

 

    string private _name;
    string private _symbol;
    uint8 private _decimals;

تُستخدم هذه المتغيرات الثلاثة لتحسين قابلية القراءة. أول اثنين واضحان بذاتهما، لكن _decimals ليس كذلك.

من ناحية، لا تحتوي إيثيريوم على متغيرات الفاصلة العائمة أو المتغيرات الكسرية. من ناحية أخرى، يحب البشر أن يكونوا قادرين على تقسيم الرموز المميزة. أحد الأسباب التي جعلت الناس يستقرون على الذهب كعملة هو أنه كان من الصعب إعطاء الباقي عندما أراد شخص ما شراء ما يعادل بطة من بقرة.

الحل هو تتبع الأعداد الصحيحة، ولكن بدلاً من حساب الرمز المميز الحقيقي، يتم حساب رمز مميز كسري لا قيمة له تقريبًا. في حالة الإيثر، يُطلق على الرمز المميز الكسري اسم Wei، و 10^18 wei يساوي 1 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.

المُنشئ

يتم استدعاء المُنشئ عند إنشاء العقد لأول مرة. حسب العرف، تتم تسمية معلمات الدالة بـ <something>_.

دوال واجهة المستخدم

تساعد هذه الدوال، name، و symbol، و decimals واجهات المستخدم على معرفة عقدك حتى تتمكن من عرضه بشكل صحيح.

نوع الإرجاع هو string memory، مما يعني إرجاع سلسلة نصية مخزنة في الذاكرة. يمكن تخزين المتغيرات، مثل السلاسل النصية، في ثلاثة مواقع:

العمر الافتراضيوصول العقدتكلفة الغاز
الذاكرة (Memory)استدعاء الدالةقراءة/كتابةالعشرات أو المئات (أعلى للمواقع الأعلى)
بيانات الاستدعاء (Calldata)استدعاء الدالةقراءة فقطلا يمكن استخدامه كنوع إرجاع، فقط كنوع معلمة دالة
التخزين (Storage)حتى يتم تغييرهقراءة/كتابةعالية (800 للقراءة، 20 ألف للكتابة)

في هذه الحالة، memory هو الخيار الأفضل.

قراءة معلومات الرمز المميز

هذه هي الدوال التي توفر معلومات حول الرمز المميز، إما إجمالي المعروض أو رصيد الحساب.

    /**
     * @dev راجع {IERC20-totalSupply}.
     */
    function totalSupply() public view override returns (uint256) {
        return _totalSupply;
    }

تُرجع الدالة totalSupply إجمالي المعروض من الرموز المميزة.

 

    /**
     * @dev راجع {IERC20-balanceOf}.
     */
    function balanceOf(address account) public view override returns (uint256) {
        return _balances[account];
    }

قراءة رصيد حساب. لاحظ أنه يُسمح لأي شخص بالحصول على رصيد حساب أي شخص آخر. لا جدوى من محاولة إخفاء هذه المعلومات، لأنها متوفرة على كل عقدة على أي حال. لا توجد أسرار على سلسلة الكتل.

تحويل الرموز المميزة

يتم استدعاء الدالة transfer لتحويل الرموز المميزة من حساب المرسل إلى حساب مختلف. لاحظ أنه على الرغم من أنها تُرجع قيمة منطقية (boolean)، إلا أن هذه القيمة تكون دائمًا true. إذا فشل التحويل، يتراجع العقد عن الاستدعاء.

 

        _transfer(_msgSender(), recipient, amount);
        return true;
    }

تقوم الدالة _transfer بالعمل الفعلي. إنها دالة خاصة لا يمكن استدعاؤها إلا بواسطة دوال العقد الأخرى. حسب العرف، تتم تسمية الدوال الخاصة بـ _<something>، تمامًا مثل متغيرات الحالة.

عادةً في Solidity نستخدم msg.sender لمرسل الرسالة. ومع ذلك، فإن هذا يكسر OpenGSN (opens in a new tab). إذا أردنا السماح بالمعاملات بدون إيثر باستخدام الرمز المميز الخاص بنا، فنحن بحاجة إلى استخدام _msgSender(). إنها تُرجع msg.sender للمعاملات العادية، ولكن بالنسبة للمعاملات بدون إيثر، تُرجع المُوقّع الأصلي وليس العقد الذي قام بترحيل الرسالة.

دوال السماحية

هذه هي الدوال التي تنفذ وظيفة السماحية: allowance، و approve، و transferFrom، و _approve. بالإضافة إلى ذلك، يتجاوز تنفيذ أوبن زبلن المعيار الأساسي ليشمل بعض الميزات التي تعمل على تحسين الأمان: increaseAllowance، و decreaseAllowance.

دالة allowance

    /**
     * @dev راجع {IERC20-allowance}.
     */
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        return _allowances[owner][spender];
    }

تسمح الدالة allowance للجميع بالتحقق من أي سماحية.

دالة approve

    /**
     * @dev راجع {IERC20-approve}.
     *
     * المتطلبات:
     *
     * - لا يمكن أن يكون `spender` هو العنوان الصفري.
     */
    function approve(address spender, uint256 amount) public virtual override returns (bool) {

يتم استدعاء هذه الدالة لإنشاء سماحية. إنها مشابهة للدالة transfer أعلاه:

  • تقوم الدالة فقط باستدعاء دالة داخلية (في هذه الحالة، _approve) تقوم بالعمل الحقيقي.
  • إما أن تُرجع الدالة true (إذا نجحت) أو تتراجع (إذا لم تنجح).

 

        _approve(_msgSender(), spender, amount);
        return true;
    }

نستخدم الدوال الداخلية لتقليل عدد الأماكن التي تحدث فيها تغييرات الحالة. أي دالة تغير الحالة هي خطر أمني محتمل يحتاج إلى تدقيق أمني. بهذه الطريقة لدينا فرص أقل لارتكاب الأخطاء.

دالة transferFrom

هذه هي الدالة التي يستدعيها المنفق لإنفاق سماحية. يتطلب هذا عمليتين: تحويل المبلغ الذي يتم إنفاقه وتقليل السماحية بهذا المبلغ.

 

يقوم استدعاء الدالة a.sub(b, "message") بشيئين. أولاً، يحسب a-b، وهي السماحية الجديدة. ثانيًا، يتحقق من أن هذه النتيجة ليست سالبة. إذا كانت سالبة، يتراجع الاستدعاء مع الرسالة المقدمة. لاحظ أنه عندما يتراجع الاستدعاء، يتم تجاهل أي معالجة تمت مسبقًا أثناء هذا الاستدعاء، لذلك لا نحتاج إلى التراجع عن _transfer.

        _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount,
             "ERC20: transfer amount exceeds allowance"));
        return true;
    }

إضافات الأمان من أوبن زبلن

من الخطير تعيين سماحية غير صفرية إلى قيمة أخرى غير صفرية، لأنك تتحكم فقط في ترتيب معاملاتك الخاصة، وليس معاملات أي شخص آخر. تخيل أن لديك مستخدمين، أليس الساذجة وبيل غير الأمين. تريد أليس بعض الخدمات من بيل، والتي تعتقد أنها تكلف خمسة رموز مميزة - لذلك تمنح بيل سماحية بخمسة رموز مميزة.

ثم يتغير شيء ما ويرتفع سعر بيل إلى عشرة رموز مميزة. ترسل أليس، التي لا تزال تريد الخدمة، معاملة تحدد سماحية بيل بعشرة. في اللحظة التي يرى فيها بيل هذه المعاملة الجديدة في مجمع المعاملات، يرسل معاملة تنفق رموز أليس الخمسة ولها سعر غاز أعلى بكثير بحيث يتم تعدينها بشكل أسرع. بهذه الطريقة يمكن لبيل إنفاق أول خمسة رموز مميزة ثم، بمجرد تعدين سماحية أليس الجديدة، إنفاق عشرة أخرى بسعر إجمالي يبلغ خمسة عشر رمزًا مميزًا، وهو أكثر مما قصدت أليس تفويضه. تُسمى هذه التقنية الاستباق (opens in a new tab)

معاملة أليسرقم أليس الفريدمعاملة بيلرقم بيل الفريدسماحية بيلإجمالي دخل بيل من أليس
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
approve(Bill, 10)11105
transferFrom(Alice, Bill, 10)10,124015

لتجنب هذه المشكلة، تتيح لك هاتان الدالتان (increaseAllowance و decreaseAllowance) تعديل السماحية بمقدار محدد. لذلك إذا كان بيل قد أنفق بالفعل خمسة رموز مميزة، فسيكون قادرًا فقط على إنفاق خمسة أخرى. اعتمادًا على التوقيت، هناك طريقتان يمكن أن يعمل بهما هذا، وكلاهما ينتهي بحصول بيل على عشرة رموز مميزة فقط:

أ:

معاملة أليسرقم أليس الفريدمعاملة بيلرقم بيل الفريدسماحية بيلإجمالي دخل بيل من أليس
approve(Bill, 5)1050
transferFrom(Alice, Bill, 5)10,12305
increaseAllowance(Bill, 5)110+5 = 55
transferFrom(Alice, Bill, 5)10,124010

ب:

معاملة أليسرقم أليس الفريدمعاملة بيلرقم بيل الفريدسماحية بيلإجمالي دخل بيل من أليس
approve(Bill, 5)1050
increaseAllowance(Bill, 5)115+5 = 100
transferFrom(Alice, Bill, 10)10,124010

الدالة a.add(b) هي إضافة آمنة. في الحالة غير المحتملة التي يكون فيها a+b>=2^256، فإنها لا تلتف (wrap around) بالطريقة التي تعمل بها الإضافة العادية.

الدوال التي تعدل معلومات الرمز المميز

هذه هي الدوال الأربع التي تقوم بالعمل الفعلي: _transfer، و _mint، و _burn، و _approve.

دالة _transfer

تقوم هذه الدالة، _transfer، بتحويل الرموز المميزة من حساب إلى آخر. يتم استدعاؤها بواسطة كل من transfer (للتحويلات من حساب المرسل نفسه) و transferFrom (لاستخدام السماحيات للتحويل من حساب شخص آخر).

 

        require(sender != address(0), "ERC20: transfer from the zero address");
        require(recipient != address(0), "ERC20: transfer to the zero address");

لا أحد يمتلك فعليًا العنوان الصفري في إيثيريوم (أي لا أحد يعرف مفتاحًا خاصًا يتم تحويل مفتاحه العام المطابق إلى العنوان الصفري). عندما يستخدم الأشخاص هذا العنوان، فعادةً ما يكون ذلك خطأ برمجيًا - لذلك نفشل إذا تم استخدام العنوان الصفري كمرسل أو مستلم.

 

        _beforeTokenTransfer(sender, recipient, amount);

هناك طريقتان لاستخدام هذا العقد:

  1. استخدامه كقالب للكود الخاص بك
  2. الوراثة منه (opens in a new tab)، وتجاوز (override) فقط تلك الدوال التي تحتاج إلى تعديلها

الطريقة الثانية أفضل بكثير لأن كود ERC-20 من أوبن زبلن قد تم تدقيقه بالفعل وثبت أنه آمن. عندما تستخدم الوراثة، يكون من الواضح ما هي الدوال التي تقوم بتعديلها، ولكي يثق الناس في عقدك، يحتاجون فقط إلى تدقيق تلك الدوال المحددة.

غالبًا ما يكون من المفيد أداء دالة في كل مرة يتم فيها تداول الرموز المميزة. ومع ذلك، فإن _transfer هي دالة مهمة جدًا ومن الممكن كتابتها بشكل غير آمن (انظر أدناه)، لذلك من الأفضل عدم تجاوزها. الحل هو _beforeTokenTransfer، وهي دالة خطافية (hook function) (opens in a new tab). يمكنك تجاوز هذه الدالة، وسيتم استدعاؤها في كل تحويل.

 

        _balances[sender] = _balances[sender].sub(amount, "ERC20: transfer amount exceeds balance");
        _balances[recipient] = _balances[recipient].add(amount);

هذه هي الأسطر التي تقوم بالتحويل الفعلي. لاحظ أنه لا يوجد شيء بينها، وأننا نطرح المبلغ المحول من المرسل قبل إضافته إلى المستلم. هذا مهم لأنه إذا كان هناك استدعاء لعقد مختلف في المنتصف، فقد يكون قد تم استخدامه لغش هذا العقد. بهذه الطريقة يكون التحويل ذريًا (atomic)، ولا يمكن أن يحدث شيء في منتصفه.

 

        emit Transfer(sender, recipient, amount);
    }

أخيرًا، قم بإصدار حدث Transfer. لا يمكن للعقود الذكية الوصول إلى الأحداث، ولكن يمكن للكود الذي يعمل خارج سلسلة الكتل الاستماع إلى الأحداث والتفاعل معها. على سبيل المثال، يمكن للمحفظة تتبع متى يحصل المالك على المزيد من الرموز المميزة.

دالتي _mint و _burn

تقوم هاتان الدالتان (_mint و _burn) بتعديل إجمالي المعروض من الرموز المميزة. إنها داخلية ولا توجد دالة تستدعيها في هذا العقد، لذلك فهي مفيدة فقط إذا ورثت من العقد وأضفت المنطق الخاص بك لتقرير تحت أي ظروف يتم سك رموز مميزة جديدة أو حرق الرموز الموجودة.

ملاحظة: كل رمز مميز ERC-20 له منطق الأعمال الخاص به الذي يملي إدارة الرمز المميز. على سبيل المثال، قد يستدعي عقد العرض الثابت _mint فقط في المُنشئ ولا يستدعي أبدًا _burn. سيستدعي العقد الذي يبيع الرموز المميزة _mint عندما يتم الدفع له، ومن المفترض أن يستدعي _burn في مرحلة ما لتجنب التضخم الجامح.

تأكد من تحديث _totalSupply عندما يتغير إجمالي عدد الرموز المميزة.

 

الدالة _burn متطابقة تقريبًا مع _mint، باستثناء أنها تسير في الاتجاه الآخر.

دالة _approve

هذه هي الدالة التي تحدد السماحيات فعليًا. لاحظ أنها تسمح للمالك بتحديد سماحية أعلى من رصيد المالك الحالي. هذا لا بأس به لأنه يتم التحقق من الرصيد في وقت التحويل، عندما يمكن أن يكون مختلفًا عن الرصيد عند إنشاء السماحية.

 

إصدار حدث Approval. اعتمادًا على كيفية كتابة التطبيق، يمكن إخبار عقد المنفق بالموافقة إما من قبل المالك أو بواسطة خادم يستمع إلى هذه الأحداث.

        emit Approval(owner, spender, amount);
    }

تعديل متغير Decimals

تقوم هذه الدالة بتعديل المتغير _decimals والذي يُستخدم لإخبار واجهات المستخدم بكيفية تفسير المبلغ. يجب عليك استدعاؤه من المُنشئ. سيكون من غير النزيه استدعاؤه في أي نقطة لاحقة، والتطبيقات غير مصممة للتعامل مع ذلك.

الخطافات (Hooks)

هذه هي الدالة الخطافية التي سيتم استدعاؤها أثناء التحويلات. إنها فارغة هنا، ولكن إذا كنت بحاجة إليها للقيام بشيء ما، فما عليك سوى تجاوزها.

الخاتمة

للمراجعة، إليك بعض أهم الأفكار في هذا العقد (في رأيي، من المحتمل أن يختلف رأيك):

  • لا توجد أسرار على سلسلة الكتل. أي معلومات يمكن للعقد الذكي الوصول إليها متاحة للعالم بأسره.
  • يمكنك التحكم في ترتيب معاملاتك الخاصة، ولكن ليس متى تحدث معاملات الأشخاص الآخرين. هذا هو السبب في أن تغيير السماحية يمكن أن يكون خطيرًا، لأنه يتيح للمنفق إنفاق مجموع كلتا السماحيتين.
  • تلتف (wrap around) قيم النوع uint256. بعبارة أخرى، 0-1=2^256-1. إذا لم يكن هذا هو السلوك المطلوب، فعليك التحقق منه (أو استخدام مكتبة SafeMath التي تقوم بذلك نيابة عنك). لاحظ أن هذا قد تغير في Solidity 0.8.0 (opens in a new tab).
  • قم بإجراء جميع تغييرات الحالة من نوع معين في مكان معين، لأن ذلك يجعل التدقيق أسهل. هذا هو السبب في أن لدينا، على سبيل المثال، _approve، والتي يتم استدعاؤها بواسطة approve، و transferFrom، و increaseAllowance، و decreaseAllowance
  • يجب أن تكون تغييرات الحالة ذرية (atomic)، دون أي إجراء آخر في منتصفها (كما ترى في _transfer). هذا لأنه أثناء تغيير الحالة يكون لديك حالة غير متسقة. على سبيل المثال، بين الوقت الذي تخصم فيه من رصيد المرسل والوقت الذي تضيف فيه إلى رصيد المستلم، يكون هناك عدد أقل من الرموز المميزة الموجودة مما ينبغي أن يكون. يمكن إساءة استخدام هذا بشكل محتمل إذا كانت هناك عمليات بينهما، خاصة الاستدعاءات لعقد مختلف.

الآن بعد أن رأيت كيف تتم كتابة عقد ERC-20 من أوبن زبلن، وخاصة كيف يتم جعله أكثر أمانًا، اذهب واكتب عقودك وتطبيقاتك الآمنة.

انظر هنا للمزيد من أعمالي (opens in a new tab).