عقد متعدد التواقيع

في هذا المقال سنقوم بإنشاء عقد متعدد التواقيع ()Multi-Sig contract ، والذي يعمل كمخزن للقيمة (يطلق عليه “المحفظة” بالمعنى التقليدي لشيء يحتفظ بالمال ، وليس محفظة تشفير مثل MetaMask). مبني على هذا المشروع بواسطة Nate Rush.

 كما أن الحل  مبني على this MultiSignature Wallet 

ما هي المحفظة متعددة التوقيعات؟

المحفظة متعددة التوقيعات هي حساب يتطلب بعض النصاب القانوني (m-of-n quorum )  للمفاتيح الخاصة المعتمدة للموافقة على معاملة قبل تنفيذها.

في Ethereum ، يتم تنفيذ المحافظ متعددة التوقيع كعقد ذكي ، حيث يرسل كل حساب خارجي معتمد معاملة من أجل “توقيع” معاملة جماعية.

باتباع مواصفات المشروع التي صممها UPenn Blockchain Club ، ستقوم الآن بإنشاء عقد محفظة متعددة التوقيعات الخاصة بك.

ملاحظة: لا يُقترح أن تستخدم هذه المحفظة متعددة التوقيعات مع أي أموال حقيقية ، بل تستخدم محفظة أكثر عمقًا مثل محفظة Gnosis multisignature wallet .

إعداد مشروع

استنساخ مستودع GitHub هذا. يحتوي ملف MultiSignatureWallet.sol 

تنفيذ العقد

انسخ محتويات ملف MultiSignatureWallet.sol الموجود في دليل المشروع إلى Remix أو Truffle أو أي IDE عقد ذكي تفضله.

دعنا نراجع ما يجب أن يكون هذا العقد قادرًا على فعله قبل أن نبدأ في كتابة الكود:

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

مع وضع هذه المتطلبات في الاعتبار ، دعنا ننتقل إلى كعب العقد ونبدأ في تنفيذ هذه الوظيفة.

Constructor

بدءًا من المُنشئ ، يمكنك أن ترى أنه مع أحدث إصدار من Solidity ، فإن استخدام اسم العقد كـ Constructor تم إهماله، لذلك دعونا نغيره إلى المُنشئ.

    constructor(address[] memory _owners, uint _required)

نرغب في التحقق من مدخلات المستخدم إلى Constructor  للتأكد من أن المستخدم لا يتطلب تأكيدات أكثر من عدد المالكين ، وأن العقد يتطلب تأكيدًا واحدًا على الأقل قبل إرسال المعاملة وأن مجموعة المالكين تحتوي على عنوان واحد على الأقل.

يمكننا إنشاء مُعدِّل يتحقق من هذه الشروط

 modifier validRequirement(uint ownerCount, uint _required) {
          if (   _required > ownerCount || _required == 0 || ownerCount == 0)
              revert();
          _;
      }
  

وندعوها عندما يعمل Constructor .

    constructor(address[] memory _owners, uint _required) public 
              validRequirement(_owners.length, _required)
          {...}

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

    address[] public owners;
      uint public required;
      mapping (address => bool) public isOwner;

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

سيتم تعيين كل هذه المتغيرات في المنشئ.

 constructor(address[] memory _owners, uint _required) public 
          validRequirement(_owners.length, _required)
      {
          for (uint i=0; i<_owners.length; i++) {
              isOwner[_owners[i]] = true;
          }
          owners = _owners;
          required = _required;
      }

إرسال المعاملة

تتيح وظيفة إرسال المعاملة للمالك إرسال وتأكيد المعاملة.

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

    require(isOwner[msg.sender]);

بالنظر إلى بقية نص العقد ، ستلاحظ وجود وظيفتين أخريين في العقد يمكن أن تساعدك في تنفيذ هذه الوظيفة ، إحداهما تسمى addTransaction والتي تأخذ نفس المدخلات مثل SubmitTransaction وتعود معاملة uint. يُطلق على الآخر اسم ConfirmTransaction الذي يأخذ معرفًا لمعاملة واحدة.

يمكننا بسهولة تنفيذ SubmitTransaction بمساعدة هذه الوظائف الأخرى:

    function submitTransaction(address destination, uint value, bytes memory data) 
          public 
          returns (uint transactionId) 
      {
          require(isOwner[msg.sender]);
          transactionId = addTransaction(destination, value, data);
          confirmTransaction(transactionId);
      }

أضف معاملة

دعنا ننتقل إلى وظيفة addTransaction وننفذ ذلك. تضيف هذه الوظيفة معاملة جديدة إلى مخطط المعاملة (الذي نحن على وشك إنشائه) ، إذا لم تكن المعاملة موجودة بعد.

    function addTransaction(address destination, uint value, bytes memory data) internal returns (uint transactionId);

المعاملة هي بنية بيانات محددة في كعب العقد.

    struct Transaction {
          address destination;
          uint value;
          bytes data;
          bool executed;
      }
  

نحتاج إلى تخزين المدخلات لوظيفة addTransaction في هيكل معاملة وإنشاء معرف معاملة للمعاملة. دعنا ننشئ متغيرين تخزين آخرين لتتبع معرّفات المعاملات وتخطيط المعاملات.

    uint public transactionCount;
      mapping (uint => Transaction) public transactions;

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

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

    event Submission(uint indexed transactionId);

تجعلindexed  في إعلان الحدث من سهولة البحث عن الحدث ومفيدة عند إنشاء واجهات مستخدم تحتاج إلى تحليل الكثير من الأحداث.

في جسم الوظيفة يمكننا استدعاء الحدث.

    function addTransaction(address destination, uint value, bytes memory data)
          internal
          returns (uint transactionId)
      {
          transactionId = transactionCount;
          transactions[transactionId] = Transaction({
              destination: destination,
              value: value,
              data: data,
              executed: false
          });
          transactionCount += 1;
          emit Submission(transactionId);
      }

يتم إرجاع معرف المعاملة uint من دالة addTransaction لتسليمها إلى وظيفة ConfirmTransaction.

تأكيد المعاملة

    function confirmTransaction(uint transactionId) public {}

تسمح وظيفة تأكيد المعاملة للمالك بتأكيد المعاملة المضافة.

يتطلب هذا متغيرًا آخر للتخزين ، وهو تعيين التأكيدات الذي يخزن تعيين القيم المنطقية على عناوين المالك. يتتبع هذا المتغير عناوين المالك التي أكدت المعاملات.

    mapping (uint => mapping (address => bool)) public confirmations;

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

    require(isOwner[msg.sender]);
      require(transactions[transactionId].destination != address(0));
      require(confirmations[transactionId][msg.sender] == false);
  

بمجرد أن تتلقى المعاملة العدد المطلوب من التأكيدات ، يجب أن يتم تنفيذ المعاملة ، لذلك بمجرد تعيين القيمة المنطقية المناسبة على “صحيح”

    confirmations[transactionId][msg.sender] = true;

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

    event Confirmation(address indexed sender, uint indexed transactionId);

تتم فهرسة كل من معلمات الحدث هذه لتسهيل البحث عن الحدث. الآن يمكننا استدعاء الحدث في الوظيفة.

بعد تسجيل الحدث يمكننا محاولة تنفيذ المعاملة

    executeTransaction(transactionId);

لذلك يجب أن تبدو الوظيفة بأكملها كما يلي:

   function confirmTransaction(uint transactionId)
          public
      {
          require(isOwner[msg.sender]);
          require(transactions[transactionId].destination != address(0));
          require(confirmations[transactionId][msg.sender] == false);
          confirmations[transactionId][msg.sender] = true;
          emit Confirmation(msg.sender, transactionId);
          executeTransaction(transactionId);
      }

تنفيذ المعاملة

تأخذ وظيفة تنفيذ المعاملة معلمة واحدة ، معرف المعاملة.

أولاً ، نريد التأكد من أن المعاملة على المعرف المحدد لم يتم تنفيذها بالفعل.

    require(transactions[transactionId].executed == false);

ثم نريد التحقق من أن المعاملة بها على الأقل العدد المطلوب من التأكيدات.

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

أنا أعرّف وظيفة المساعد isConfirmed ، والتي يمكننا استدعاؤها من دالة executeTransaction.

   function isConfirmed(uint transactionId)
          public
          view
          returns (bool)
      {
          uint count = 0;
          for (uint i=0; i<owners.length; i++) {
              if (confirmations[transactionId][owners[i]])
                  count += 1;
              if (count == required)
                  return true;
          }
      }
  

  

سترجع True إذا تم تأكيد المعاملة ، لذلك في وظيفة تنفيذ المعاملة ، يمكننا تنفيذ المعاملة في المحدد إذا تم تأكيدها ، وإلا لا ننفذها ثم قم بتحديث بنية المعاملة لتعكس الحالة.

نحن نقوم بتحديث الحالة ، لذلك يجب علينا تسجيل حدث يعكس التغيير.

    event Execution(uint indexed transactionId);
      event ExecutionFailure(uint indexed transactionId);

لدينا نتيجتان محتملتان لهذه الوظيفة – المعاملة غير مضمونة أن تنفذ بنجاح. سنرغب في فهرسة ما إذا كانت المعاملة المرسلة ستنفذ بنجاح أم لا. لاحظ كيف اتعامل الوظيفة مع معاملة فاشلة ويتتبعها في حالة العقد.

    function executeTransaction(uint transactionId)
          public
      {
          require(transactions[transactionId].executed == false);
          if (isConfirmed(transactionId)) {
              Transaction storage t = transactions[transactionId];  // using the "storage" keyword makes "t" a pointer to storage 
              t.executed = true;
              (bool success, bytes memory returnedData) = t.destination.call.value(t.value)(t.data);
              if (success)
                  emit Execution(transactionId);
              else {
                  emit ExecutionFailure(transactionId);
                  t.executed = false;
              }
          }
      }

وظائف اضافيه

حتى الآن ، قمنا فقط بتغطية الوظائف الأساسية لمحفظة MultiSignature .

سأترك الأمر لك لمواصلة التمرين واستكشاف بقية العقد. تم التعليق على الكود جيدًا ويجب أن تكون قادرًا على تحديد وشرح الغرض من كل وظيفة في العقد.

إذا كنت ترغب في مزيد من التحدي ، فتابع إلى أسفل ملف Solidity وتحقق من عقد MultiSigWalletWithDailyLimit contract.

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

التعامل مع العقد

الآن بعد أن أصبح لدينا محفظة MultiSignature أساسية ، دعنا نتفاعل مع محفظة Multisig ونرى كيف تعمل.

انسخ العقد الذي قمنا بتطويره في Remix في دليل مشروع truffle المقدم.

يمكنك أن ترى أن دليل المشروع يأتي مع عقد SimpleStorage.sol. هذا هو العقد الذي سنطلبه من عقد Multisig.

إذا نظرت في دليل migrations ، فسترى النص البرمجي للنشر الذي سيستخدمه truffle لنشر عقد SimpleStorage بالإضافة إلى محفظة MultiSig.

يسمح لنا ناشر truffle  بالوصول إلى الحسابات ، وهو أمر مفيد نظرًا لأن مُنشئ العقد MultiSig يتطلب مجموعة من عناوين المالك بالإضافة إلى عدد التأكيدات المطلوبة لتنفيذ المعاملة.

مصفوفة المالكين والبرامج النصية للنشر موجودة بالفعل في الملف.

    const owners = [accounts[0], accounts[1]]
      deployer.deploy(MultiSig, owners, 2)
  

سنطلب تأكيدين فقط من أجل البساطة.

لنشر العقود ، ابدأ بيئة التطوير بتشغيل truffle develop  في نافذة طرفية في دليل المشروع. سيظهر سطر أوامر truffle 

truffle(develop)>

نشر العقود

truffle(develop)> migrate

إذا لم تنجح عملية الترحيل ، فحاول ترحيل migrate –reset.

ثم احصل على المثيلات المنشورة لعقود SimpleStorage.sol و MultiSignatureWallet.sol.

truffle(develop)> var ss = await SimpleStorage.at(SimpleStorage.address)
  truffle(develop)> var ms = await MultiSignatureWallet.at(MultiSignatureWallet.address)

تحقق من حالة عقد SimpleStorage

truffle(develop)> ss.storedData.call()
  <BN: 0>
  

هذا يعني أنه 0. يمكنك التحقق من خلال انتظار الوعد بحل وتحويل الإجابة إلى سلسلة. جربها مع:

ss.storedData.call().then(res => { console.log( res.toString(10) )} )
  0
  

دعنا نرسل معاملة لتحديث حالة عقد SimpleStorage لعقد MultiSig. يأخذ SumbitTransaction عنوان عقد الوجهة والقيمة المراد إرسالها مع المعاملة وبيانات المعاملة ، والتي تتضمن توقيع الوظيفة المشفرة ومعلمات الإدخال.

إذا أردنا تحديث بيانات عقد SimpleStorage لتصبح 5 ، سيبدو توقيع الوظيفة المشفرة ومعلمات الإدخال كما يلي:

var encoded = '0x60fe47b10000000000000000000000000000000000000000000000000000000000000005'

دعنا نحصل على الحسابات المتاحة ثم نقوم بالاتصال بعقد MultiSig:

truffle(develop)> var accounts = await web3.eth.getAccounts()
  truffle(develop)> ms.submitTransaction(ss.address, 0, encoded, {from: accounts[0]})

و نرى معلومات المعاملة مطبوعة في نافذة terminal . في السجلات ، يمكننا أن نرى أنه تم إطلاق حدث “إرسال” ، وكذلك حدث “تأكيد” ، وهو ما نتوقعه.

الحالة الحالية لـ MultiSig لها معاملة واحدة لم يتم تنفيذها ولديها تأكيد واحد (من العنوان الذي أرسلها). يجب أن يؤدي تأكيد واحد آخر إلى تنفيذ المعاملة. دعنا نستخدم الحساب الثاني لتأكيده. تأخذ وظيفة ConfirmTransaction إدخالًا واحدًا ، وهو فهرس المعاملة للتأكيد.

truffle(develop)> ss.storedData.call()
  <BN: 5>

تتم طباعة معلومات المعاملة في terminal . يجب أن تشاهد حدثين في السجل هذه المرة أيضًا. حدث “تأكيد” وكذلك حدث “تنفيذ”. يشير هذا إلى أن استدعاء SimpleStorage تم تنفيذه بنجاح. إذا لم يتم تنفيذه بنجاح ، فسنرى حدث “ExecutionFailure” هناك بدلاً من ذلك.

يمكننا التحقق من تحديث حالة العقد عن طريق التشغيل

truffle(develop)> ss.storedData.call()
  <BN: 5>
  

أصبحت البيانات المخزنة الآن 5. ويمكننا التحقق من أن العنوان الذي تم تحديث عقد SimpleStorage كان هو MultiSig Wallet.

truffle(develop)> ss.caller.call()
  ‘0x855d1c79ad3fb086d516554dc7187e3fdfc1c79a'
  truffle(develop)> ms.address
  ‘0x855d1c79ad3fb086d516554dc7187e3fdfc1c79a'

العنوانان متماثلان!

تهانينا! لقد قمت للتو بإنشاء محفظة متعددة التوقيع تستخدم التحكم الأساسي في الوصول!

مصادر إضافية 

  • يوتيوب : https://www.youtube.com/playlist?list=PLO5VPQH6OWdVfvNOaEhBtA53XHyHo_oJo
  • مقال : https://solidity-by-example.org/app/multi-sig-wallet/

إضافة تعليق