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

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

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

مقدمة

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

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

العقد

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

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

from vyper.interfaces import ERC721

implements: ERC721

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

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

واجهة 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 bits. عادةً ما يتم إنشاؤها عن طريق عملية التجزئة لوصف ما يمثله الرمز المميز.

            _data: Bytes[1024]

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

        ) -> bytes32: view

لمنع الحالات التي يقبل فيها العقد تحويلاً عن طريق الخطأ، فإن القيمة المرجعة ليست قيمة منطقية (boolean)، بل 256 bits بقيمة محددة.

هذه الدالة هي view، مما يعني أنه يمكنها قراءة حالة سلسلة الكتل، ولكن لا يمكنها تعديلها.

أحداث

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

هذا مشابه لحدث التحويل في ERC-20، باستثناء أننا نبلغ عن tokenId بدلاً من مبلغ. لا أحد يمتلك العنوان الصفري، لذلك حسب العرف نستخدمه للإبلاغ عن إنشاء وتدمير الرموز المميزة.

الموافقة في ERC-721 مشابهة للسماحية في ERC-20. يُسمح لعنوان محدد بتحويل رمز مميز محدد. يوفر هذا آلية للعقود للاستجابة عندما تقبل رمزًا مميزًا. لا يمكن للعقود الاستماع إلى الأحداث، لذلك إذا قمت فقط بتحويل الرمز المميز إليها، فلن "تعرف" بذلك. بهذه الطريقة، يقدم المالك أولاً موافقة ثم يرسل طلبًا إلى العقد: "لقد وافقت لك على تحويل الرمز المميز X، يرجى القيام بـ ...".

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

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

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

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

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

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

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

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

# @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 تعيين معرف الواجهة إلى قيمة منطقية حول ما إذا كانت مدعومة أم لا
supportedInterfaces: HashMap[bytes32, bool]

# @dev معرف واجهة ERC-165 لـ ERC-165
ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a7

# @dev معرف واجهة ERC-165 لـ ERC-721
ERC721_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000080ac58cd

يحدد ERC-165 (opens in a new tab) آلية للعقد للكشف عن كيفية تواصل التطبيقات معه، وإلى أي معايير ERC يتوافق. في هذه الحالة، يتوافق العقد مع ERC-165 و ERC-721.

الدوال

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

المُنشئ

@external
def __init__():

في لغة Vyper، كما هو الحال في لغة Python، تُسمى دالة المُنشئ __init__.

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

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

    self.supportedInterfaces[ERC165_INTERFACE_ID] = True
    self.supportedInterfaces[ERC721_INTERFACE_ID] = True
    self.minter = msg.sender

للوصول إلى متغيرات الحالة، تستخدم self.<variable name> (مرة أخرى، كما هو الحال في لغة Python).

دوال العرض (View Functions)

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

@view
@external

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

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

على النقيض من لغة Python، فإن لغة Vyper هي لغة ذات كتابة ثابتة (static typed language) (opens in a new tab). لا يمكنك الإعلان عن متغير، أو معلمة دالة، دون تحديد نوع البيانات (opens in a new tab). في هذه الحالة، معلمة الإدخال هي bytes32، وهي قيمة بحجم 256-bit (256 bits هو حجم الكلمة الأصلي لـ آلة إيثيريوم الافتراضية). المخرجات عبارة عن قيمة منطقية (boolean). حسب العرف، تبدأ أسماء معلمات الدالة بشرطة سفلية (_).

    """
    @dev تم تحديد تعريف الواجهة في ERC-165.
    @param _interfaceID معرف الواجهة
    """
    return self.supportedInterfaces[_interfaceID]

إرجاع القيمة من خريطة التجزئة (HashMap) self.supportedInterfaces، والتي تم تعيينها في المُنشئ (__init__).

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

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

هذا السطر يؤكد (asserts) (opens in a new tab) أن _owner ليس صفرًا. إذا كان كذلك، فهناك خطأ ويتم التراجع عن العملية.

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

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

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

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

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


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

@view
@internal

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

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

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

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

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

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

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

لإصدار حدث في لغة Vyper، تستخدم عبارة log (انظر هنا لمزيد من التفاصيل (opens in a new tab)).

دوال التحويل

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

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

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

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

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

استدعِ العقد المستهدف لمعرفة ما إذا كان يمكنه استلام الرموز المميزة ERC-721.

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

إذا كانت الوجهة عبارة عن عقد، ولكنه لا يقبل الرموز المميزة 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، فإنك تأخذ هذا العقد (opens in a new tab) وتقوم بتعديله لتنفيذ منطق الأعمال الذي تريده.

الخاتمة

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

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

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

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