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

جولة تفصيلية في عقد ⁦ERC-721⁩ بلغة ⁦Vyper⁩

Vyper
erc-721
Python
مبتدئ
أوري بوميرانتس
1 أبريل 2021
19 دقيقة للقراءة

مقدمة

يُستخدم معيار ERC-721 للاحتفاظ بملكية الرموز المميزة غير القابلة للاستبدال (NFT). تتصرف الرموز المميزة ERC-20 كسلعة، لأنه لا يوجد فرق بين الرموز المميزة الفردية. على النقيض من ذلك، تم تصميم الرموز المميزة ERC-721 للأصول المتشابهة ولكنها غير متطابقة، مثل الرسوم المتحركة المختلفة للقطط (يفتح في علامة تبويب جديدة) أو سندات الملكية لقطع عقارية مختلفة.

في هذه المقالة سنقوم بتحليل عقد ERC-721 الخاص بـ ريويا ناكامورا (يفتح في علامة تبويب جديدة). هذا العقد مكتوب بلغة Vyper (يفتح في علامة تبويب جديدة)، وهي لغة عقود تشبه لغة Python مصممة لجعل كتابة التعليمات البرمجية غير الآمنة أكثر صعوبة مما هي عليه في لغة Solidity.

العقد

# @dev تنفيذ معيار الرمز المميز غير القابل للاستبدال ERC-721.
# @author Ryuya Nakamura (@nrryuya)
# معدل من: https://github.com/vyperlang/vyper/blob/de74722bf2d8718cca46902be165f9fe0e3641dd/examples/tokens/ERC721.vy

تبدأ التعليقات في لغة Vyper، كما هو الحال في لغة Python، بعلامة التجزئة (ethereum.ercs) وتستمر حتى نهاية السطر. تُستخدم التعليقات التي تتضمن @<keyword> بواسطة NatSpec (يفتح في علامة تبويب جديدة) لإنتاج وثائق قابلة للقراءة من قبل البشر.

from vyper.interfaces import ERC721

implements: ERC721

واجهة ERC-721 مدمجة في لغة Vyper. يمكنك رؤية تعريف التعليمات البرمجية هنا (يفتح في علامة تبويب جديدة). تعريف الواجهة مكتوب بلغة Python، بدلاً من Vyper، لأن الواجهات لا تُستخدم فقط داخل سلسلة الكتل، ولكن أيضًا عند إرسال معاملة إلى سلسلة الكتل من عميل خارجي، والذي قد يكون مكتوبًا بلغة Python.

يستورد السطر الأول الواجهة، ويحدد السطر الثاني أننا نقوم بتنفيذها هنا.

#pragma version >0.3.10
#pragma version >0.3.10

واجهة ERC721Receiver

# واجهة العقد الذي يتم استدعاؤه بواسطة safeTransferFrom()
interface ERC721Receiver:
    def onERC721Received(

يدعم ERC-721 نوعين من التحويل:

  • transferFrom، والذي يتيح للمرسل تحديد أي عنوان وجهة ويضع مسؤولية التحويل على المرسل. هذا يعني أنه يمكنك التحويل إلى عنوان غير صالح، وفي هذه الحالة يُفقد الرمز المميز غير القابل للاستبدال (NFT) إلى الأبد.
  • safeTransferFrom، والذي يتحقق مما إذا كان عنوان الوجهة عبارة عن عقد. إذا كان الأمر كذلك، يسأل عقد ERC-721 العقد المتلقي عما إذا كان يريد استلام الرمز المميز غير القابل للاستبدال (NFT).

للرد على طلبات safeTransferFrom، يجب أن ينفذ العقد المتلقي ERC721Receiver.

            _operator: address,
            _from: address,

العنوان _from هو المالك الحالي للرمز المميز. العنوان _operator هو العنوان الذي طلب التحويل (قد لا يكون هذان العنوانان متطابقين، بسبب السماحيات). حسب العرف، تبدأ معظم معلمات الدوال في هذا العقد بشرطة سفلية (_).

            _tokenId: uint256,

تتكون معرفات رموز ERC-721 المميزة من 256 بت. عادةً ما يتم إنشاؤها عن طريق عملية التجزئة لوصف ما يمثله الرمز المميز.

            _data: Bytes[1024]

يمكن أن يحتوي الطلب على ما يصل إلى 1024 بايت من بيانات المستخدم.

        ) -> bytes4: nonpayable

لمنع الحالات التي يقبل فيها العقد تحويلاً عن طريق الخطأ، فإن القيمة المرجعة ليست قيمة منطقية (boolean)، بل قيمة محددة مكونة من أربعة بايت، وهي محدد الدالة (function selector) لـ onERC721Received. الدالة هي nonpayable لأن العقد المتلقي قد يغير حالته الخاصة عندما يقبل رمزًا مميزًا.

الأحداث

يتم إصدار الأحداث لإعلام المستخدمين والخوادم خارج سلسلة الكتل بالأحداث. لاحظ أن محتوى الأحداث غير متاح للعقود الموجودة على سلسلة الكتل. يتم تعريف أحداث ERC-721 الثلاثة بواسطة واجهة IERC721 التي استوردناها، لذلك لا يعلن هذا العقد عنها بنفسه؛ بل يصدرها باستخدام log IERC721.<Event>(...)، كما سنرى في دوال التحويل أدناه.

يُبلغ Transfer (sender، receiver، token_id) عن تغيير في ملكية رمز مميز غير قابل للاستبدال (NFT). هذا مشابه لحدث التحويل في ERC-20، باستثناء أننا نبلغ عن token_id بدلاً من مبلغ. لا أحد يمتلك العنوان الصفري، لذلك حسب العرف نستخدمه للإبلاغ عن إنشاء وتدمير الرموز المميزة. الاستثناء الوحيد هو إنشاء العقد، والذي يمكن خلاله إنشاء وتعيين أي عدد من الرموز المميزة غير القابلة للاستبدال (NFTs) دون إصدار Transfer.

تشبه الموافقة في ERC-721 السماحية في ERC-20: يُسمح لعنوان معين بتحويل رمز مميز معين، ويتم إصدار Approval (owner، approved، token_id) كلما تم تعيين هذا العنوان المعتمد أو إعادة تأكيده. يوفر هذا آلية للعقود للرد عندما تقبل رمزًا مميزًا. لا يمكن للعقود الاستماع إلى الأحداث، لذلك إذا قمت بمجرد تحويل الرمز المميز إليها فإنها لا "تعرف" بذلك. بهذه الطريقة يقدم المالك أولاً موافقة ثم يرسل طلبًا إلى العقد: "لقد وافقت لك على تحويل الرمز المميز X، يرجى القيام بـ ...". هذا خيار تصميمي لجعل معيار ERC-721 مشابهًا لمعيار ERC-20. نظرًا لأن رموز ERC-721 المميزة غير قابلة للاستبدال، يمكن للعقد أيضًا تحديد أنه حصل على رمز مميز معين من خلال النظر في ملكية الرمز المميز.

أخيرًا، يتم إصدار ApprovalForAll (owner، operator، approved) عند تمكين أو تعطيل مشغل (operator) لمالك. من المفيد أحيانًا أن يكون لديك مشغل يمكنه إدارة جميع الرموز المميزة لحساب من نوع معين (تلك التي يديرها عقد معين)، على غرار التوكيل الرسمي. على سبيل المثال، قد أرغب في إعطاء مثل هذه الصلاحية لعقد يتحقق مما إذا كنت لم أتصل به لمدة ستة أشهر، وإذا كان الأمر كذلك يوزع أصولي على ورثتي (إذا طلب أحدهم ذلك، لا يمكن للعقود أن تفعل أي شيء دون أن يتم استدعاؤها بواسطة معاملة). في ERC-20 يمكننا فقط إعطاء سماحية عالية لعقد الميراث، ولكن هذا لا ينجح مع ERC-721 لأن الرموز المميزة غير قابلة للاستبدال. هذا هو المعادل لذلك. تخبرنا القيمة approved ما إذا كان الحدث مخصصًا لموافقة، أو لسحب موافقة.

متغيرات الحالة

تحتوي هذه المتغيرات على الحالة الحالية للرموز المميزة: أي منها متاح ومن يمتلكها. معظم هذه المتغيرات عبارة عن كائنات HashMap، وهي تعيينات أحادية الاتجاه موجودة بين نوعين (يفتح في علامة تبويب جديدة).

# @dev تعيين من معرف الرمز المميز غير القابل للاستبدال (NFT) إلى العنوان الذي يمتلكه.
idToOwner: HashMap[uint256, address]

# @dev تعيين من معرف الرمز المميز غير القابل للاستبدال (NFT) إلى العنوان المعتمد.
idToApprovals: HashMap[uint256, address]

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

# @dev تعيين من عنوان المالك إلى عدد رموزه المميزة.
ownerToNFTokenCount: HashMap[address, uint256]

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

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

# @dev تعيين من عنوان المالك إلى تعيين عناوين المشغلين.
ownerToOperators: HashMap[address, HashMap[address, bool]]

قد يكون للحساب أكثر من مشغل واحد. لا يكفي استخدام HashMap بسيط لتتبعهم، لأن كل مفتاح يؤدي إلى قيمة واحدة. بدلاً من ذلك، يمكنك استخدام HashMap[address, bool] كقيمة. افتراضيًا، تكون القيمة لكل عنوان هي False، مما يعني أنه ليس مشغلاً. يمكنك تعيين القيم إلى True حسب الحاجة.

# @dev عنوان الساك، الذي يمكنه سك رمز مميز
minter: address

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

# @dev قائمة ثابتة بمعرفات واجهة ERC165 المدعومة
SUPPORTED_INTERFACES: constant(bytes4[2]) = [
    # معرف واجهة ERC165 لـ ERC165
    0x01ffc9a7,
    # معرف واجهة ERC165 لـ ERC721
    0x80ac58cd,
]

يحدد ERC-165 (يفتح في علامة تبويب جديدة) آلية للعقد للكشف عن كيفية تواصل التطبيقات معه، وإلى أي معايير ERC يتوافق. SUPPORTED_INTERFACES هي قائمة ثابتة بمعرفي الواجهة المكونين من أربعة بايت واللذين يتوافق معهما هذا العقد: ERC-165 نفسه وERC-721.

الدوال

هذه هي الدوال التي تنفذ ERC-721 فعليًا.

المُنشئ

@deploy
def __init__():

في لغة Vyper، كما هو الحال في لغة Python، تُسمى دالة المُنشئ __init__. يتم تمييزها بالزخرفة (decoration) @deploy، مما يعني أنها تعمل مرة واحدة، عند نشر العقد.

    """
    @dev مُنشئ العقد.
    """

في لغة Python، وفي لغة Vyper، يمكنك أيضًا إنشاء تعليق عن طريق تحديد سلسلة نصية متعددة الأسطر (والتي تبدأ وتنتهي بـ """)، وعدم استخدامها بأي شكل من الأشكال. يمكن أن تتضمن هذه التعليقات أيضًا NatSpec (يفتح في علامة تبويب جديدة).

    self.minter = msg.sender

للوصول إلى متغيرات الحالة، تستخدم self.<variable name> (مرة أخرى، كما هو الحال في لغة Python). يسجل المُنشئ الحساب الذي قام بنشر العقد كـ minter (الساك).

دوال العرض

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

@view
@external

تُسمى هذه الكلمات الرئيسية التي تسبق تعريف الدالة والتي تبدأ بعلامة "at" (@) الزخارف (decorations). وهي تحدد الظروف التي يمكن فيها استدعاء الدالة.

  • يحدد @view أن هذه الدالة هي دالة عرض.
  • يحدد @external أنه يمكن استدعاء هذه الدالة المحددة بواسطة المعاملات والعقود الأخرى.
def supportsInterface(interface_id: bytes4) -> bool:

على النقيض من لغة Python، فإن لغة Vyper هي لغة ذات كتابة ثابتة (static typed) (يفتح في علامة تبويب جديدة). لا يمكنك الإعلان عن متغير، أو معلمة دالة، دون تحديد نوع البيانات (يفتح في علامة تبويب جديدة). في هذه الحالة، معلمة الإدخال هي bytes4، وهي قيمة مكونة من أربعة بايت، والمخرجات عبارة عن قيمة منطقية.

    """
    @dev تم تحديد تعريف الواجهة في ERC-165.
    @param interface_id معرف الواجهة
    """
    return interface_id in SUPPORTED_INTERFACES

تُرجع True إذا كان interface_id أحد معرفات الواجهة في قائمة SUPPORTED_INTERFACES.

### دوال العرض ###

هذه هي دوال العرض التي تجعل المعلومات حول الرموز المميزة متاحة للمستخدمين والعقود الأخرى.

هذا السطر يؤكد (asserts) (يفتح في علامة تبويب جديدة) أن _owner ليس العنوان الصفري، والمكتوب كـ empty(address). إذا كان كذلك، فهناك خطأ ويتم التراجع عن العملية.

في آلة إيثيريوم الافتراضية (EVM)، أي مساحة تخزين لا تحتوي على قيمة مخزنة فيها تكون صفرًا. إذا لم يكن هناك رمز مميز عند _tokenId، فإن قيمة self.idToOwner[_tokenId] تكون صفرًا. في هذه الحالة، تتراجع الدالة.

لاحظ أن getApproved يمكن أن تُرجع صفرًا. إذا كان الرمز المميز صالحًا، فإنها تُرجع self.idToApprovals[_tokenId]. إذا لم يكن هناك معتمد، فإن هذه القيمة تكون صفرًا.

تتحقق هذه الدالة مما إذا كان يُسمح لـ _operator بإدارة جميع الرموز المميزة الخاصة بـ _owner في هذا العقد. نظرًا لأنه يمكن أن يكون هناك عدة مشغلين، فهذه HashMap ذات مستويين.

دوال المساعدة في التحويل

تنفذ هذه الدوال العمليات التي تعد جزءًا من تحويل أو إدارة الرموز المميزة.


### دوال المساعدة في التحويل ###

@view
@internal

تعني هذه الزخرفة، @internal، أنه لا يمكن الوصول إلى الدالة إلا من دوال أخرى داخل نفس العقد. حسب العرف، تبدأ أسماء هذه الدوال أيضًا بشرطة سفلية (_).

هناك ثلاث طرق يمكن من خلالها السماح لعنوان بتحويل رمز مميز:

  1. العنوان هو مالك الرمز المميز
  2. العنوان معتمد لإنفاق هذا الرمز المميز
  3. العنوان هو مشغل لمالك الرمز المميز

يمكن أن تكون الدالة أعلاه دالة عرض لأنها لا تغير الحالة. لتقليل تكاليف التشغيل، أي دالة يمكن أن تكون دالة عرض يجب أن تكون دالة عرض.

عندما تكون هناك مشكلة في التحويل، فإننا نتراجع عن الاستدعاء.

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

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

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

دوال التحويل

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

الزخرفة @payable موجودة هنا لأن واجهة IERC721 تعلن عن transferFrom وsafeTransferFrom وapprove كدوال قابلة للدفع (payable)، لذا يجب أن يتطابق العقد الذي ينفذ الواجهة مع هذه التوقيعات.

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

    if _to.is_contract: # التحقق مما إذا كان `_to` عنوان عقد

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

        returnValue: bytes4 = extcall ERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data)

استدعِ العقد المستهدف لمعرفة ما إذا كان يمكنه تلقي رموز ERC-721 المميزة. يتطلب الإصدار 0.4 من لغة Vyper تمييز الاستدعاءات للعقود الأخرى، لذلك يتم إضافة البادئة extcall إلى الاستدعاء.

        # تطرح خطأ إذا كانت وجهة التحويل عبارة عن عقد لا ينفذ 'onERC721Received'
        assert returnValue == method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes4)

إذا كانت الوجهة عبارة عن عقد، ولكنه لا يقبل رموز ERC-721 المميزة (أو قرر عدم قبول هذا التحويل المحدد)، فتراجع.

حسب العرف، إذا كنت لا ترغب في وجود معتمد، فإنك تعين العنوان الصفري، وليس نفسك.

    # التحقق من المتطلبات
    senderIsOwner: bool = self.idToOwner[_tokenId] == msg.sender
    senderIsApprovedForAll: bool = (self.ownerToOperators[owner])[msg.sender]
    assert (senderIsOwner or senderIsApprovedForAll)

لتعيين موافقة، يمكنك إما أن تكون المالك، أو مشغلاً مصرحًا له من قبل المالك.

سك رموز مميزة جديدة وتدمير الرموز الحالية

الحساب الذي أنشأ العقد هو minter، وهو المستخدم المتميز المخول لسك رموز مميزة غير قابلة للاستبدال (NFTs) جديدة. ومع ذلك، حتى هو غير مسموح له بحرق الرموز المميزة الحالية. فقط المالك، أو كيان مفوض من قبل المالك، يمكنه القيام بذلك.

### دوال السك والحرق ###

@external
def mint(_to: address, _tokenId: uint256) -> bool:

تُرجع هذه الدالة دائمًا True، لأنه إذا فشلت العملية يتم التراجع عنها.

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

    # يتراجع إذا كان `_to` هو العنوان الصفري
    assert _to != ZERO_ADDRESS
    # إضافة رمز مميز غير قابل للاستبدال. يتراجع إذا كان `_tokenId` مملوكًا لشخص ما
    self._addTokenTo(_to, _tokenId)
    log Transfer(ZERO_ADDRESS, _to, _tokenId)
    return True

حسب العرف، تُعد عملية سك الرموز المميزة الجديدة بمثابة تحويل من العنوان الصفري.

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

استخدام هذا العقد

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

الخاتمة

للمراجعة، إليك بعض أهم الأفكار في هذا العقد:

  • لاستلام الرموز المميزة ERC-721 بتحويل آمن، يجب أن تنفذ العقود واجهة ERC721Receiver.
  • حتى إذا كنت تستخدم التحويل الآمن، فلا يزال من الممكن أن تعلق الرموز المميزة إذا أرسلتها إلى عنوان يكون مفتاحه الخاص غير معروف.
  • عندما تكون هناك مشكلة في عملية ما، فمن الجيد revert الاستدعاء (التراجع عنه)، بدلاً من مجرد إرجاع قيمة فشل.
  • توجد الرموز المميزة ERC-721 عندما يكون لها مالك.
  • هناك ثلاث طرق لتكون مخولاً لتحويل رمز مميز غير قابل للاستبدال (NFT). يمكنك أن تكون المالك، أو أن تكون معتمدًا لرمز مميز محدد، أو أن تكون مشغلاً لجميع الرموز المميزة للمالك.
  • الأحداث الماضية مرئية فقط خارج سلسلة الكتل. لا يمكن للتعليمات البرمجية التي تعمل داخل سلسلة الكتل عرضها.

اذهب الآن وقم بتنفيذ عقود Vyper آمنة.

انظر هنا للمزيد من أعمالي (يفتح في علامة تبويب جديدة).