ترقية العقود الذكية (Upgradeable Contracts): كيف تحدث الكود بعد نشره على البلوكتشين؟

دقائق القراءة: 6

ترقية العقود الذكية (Upgradeable Contracts): كيف تحدث الكود بعد نشره على البلوكتشين؟

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

هنا يظهر مفهوم Upgradeable Contracts، وهو أسلوب معماري يسمح بتحديث منطق التنفيذ دون تغيير عنوان العقد الذي يتعامل معه المستخدمون أو الواجهة الأمامية. لفهم هذه الفكرة جيداً، من المفيد أولاً الإلمام بأساسيات Web3 والبلوكتشين، ثم التعمق في مكتبة OpenZeppelin لأنها العمود الفقري لأغلب تطبيقات الترقية الحديثة.

لماذا لا يمكن تحديث العقد الذكي بشكل مباشر؟

عند نشر عقد مكتوب بلغة Solidity على شبكة مثل Ethereum، يتم تخزين bytecode في عنوان ثابت. هذا العنوان يصبح مرجعاً دائماً للتطبيقات، المحافظ، والمستخدمين.

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

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

لهذا السبب ظهرت أنماط مثل Proxy Pattern، حيث يتم فصل التخزين عن منطق التنفيذ.

الفكرة الأساسية: عقد وسيط وعقد منطق

في بنية الترقية، لا يتعامل المستخدم غالباً مع عقد المنطق مباشرة، بل مع عقد وسيط يسمى Proxy. هذا العقد يحتفظ بالبيانات داخل التخزين، ثم يمرر الاستدعاءات إلى عقد آخر يسمى Implementation.

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

ماذا يستفيد المطور من هذا الفصل؟

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

أشهر أنماط الترقية في العقود الذكية

1) النمط الشفاف Transparent Proxy

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

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

2) النمط الذاتي UUPS

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

أي خطأ في منطق الترقية أو في حماية الدالة المسؤولة عن upgrade قد يسمح لمهاجم بالسيطرة الكاملة على العقد. لذلك يجب دائماً إجراء Security Auditing واختبارات صارمة قبل أي ترقية على الشبكات الحقيقية.

لماذا لا نستخدم constructor في العقود القابلة للترقية؟

العقد المنطقي في بيئة الترقية لا يُستخدم بالطريقة التقليدية نفسها، لذلك لا نعتمد على constructor لتهيئة الحالة. بدلاً من ذلك نستخدم دوال تهيئة مثل initialize.

هذه الفكرة ترتبط مباشرة بفهم الفروقات بين الذاكرة والتخزين داخل العقود، وهي نقطة تناولناها سابقاً في إدارة الذاكرة بذكاء: الفرق الحاسم بين Storage, Memory, و Calldata.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";

contract CounterV1 is Initializable, OwnableUpgradeable, UUPSUpgradeable {
    uint256 public count;

    function initialize() public initializer {
        __Ownable_init(msg.sender);
        __UUPSUpgradeable_init();
        count = 0;
    }

    function increment() external {
        count += 1;
    }

    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
}

في المثال السابق استخدمنا:

  • Initializable بدلاً من constructor.
  • OwnableUpgradeable لتقييد صلاحية الترقية.
  • UUPSUpgradeable لبناء آلية التحديث.

إضافة نسخة جديدة من العقد

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

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "./CounterV1.sol";

contract CounterV2 is CounterV1 {
    function decrement() external {
        require(count > 0, "Count is already zero");
        count -= 1;
    }
}

هنا أضفنا فقط دالة جديدة دون تغيير موقع المتغير count. لو قمت بإعادة ترتيب المتغيرات أو حذف متغير مستخدم سابقاً، قد تتلف البيانات المخزنة داخل storage slots.

كيف تتم الترقية عملياً باستخدام Hardhat؟

إذا كنت قد أنهيت سابقاً تثبيت إطار عمل Hardhat وإعداد مشروع Hardhat، فستكون عملية الترقية منظمة وسهلة نسبياً باستخدام إضافة ترقيات OpenZeppelin Upgrades.

// مثال لعقد V1 و V2 فقط، أما الترقية نفسها فتتم عبر سكربت JavaScript في Hardhat.

الخطوات العامة تكون كالتالي:

  1. كتابة النسخة الأولى من العقد باستخدام مكتبات upgradeable.
  2. نشر proxy بدلاً من نشر العقد المنطقي فقط.
  3. تطوير نسخة جديدة من العقد مع الحفاظ على توافق التخزين.
  4. تشغيل سكربت الترقية من خلال الحساب الإداري.
  5. إعادة اختبار الدوال القديمة والجديدة بعد التحديث.

ومن الضروري قبل التنفيذ على شبكة عامة أن تبني سيناريوهات اختبار شاملة كما شرحنا في اختبار العقود الذكية محلياً: كتابة اختبارات الوحدة باستخدام Chai & Mocha، لأن أي خلل في الترقية قد يكون مكلفاً وغير قابل للعكس بسهولة.

أهم المخاطر الفنية في العقود القابلة للترقية

1) تلف التخزين Storage Collision

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

2) نسيان تهيئة العقد

لو تُركت دالة initialize بدون حماية صحيحة، قد يتمكن طرف خارجي من استدعائها أولاً والحصول على دور المالك. فهم المعدلات Modifiers ضروري جداً هنا لتقييد الوصول.

3) ترقية منطق يحتوي على ثغرة

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

من أفضل ممارسات Gas Optimization في العقود القابلة للترقية: تقليل التعقيد داخل دوال التهيئة، وتجنب التخزين غير الضروري، وعدم إضافة متغيرات حالة لا حاجة لها، لأن كل كتابة إلى storage لها تكلفة فعلية على الشبكة. وللتوسع أكثر راجع التكاليف Gas Fees وفهم view و pure لتوفير رسوم Gas.

متى تستخدم العقود القابلة للترقية ومتى تتجنبها؟

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

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

خاتمة

فكرة Upgradeable Smart Contracts تحل مشكلة جوهرية في عالم العقود الذكية: كيف نطوّر النظام دون كسر البيانات أو تغيير العنوان العام. لكن هذه المرونة تأتي مع تكلفة هندسية وأمنية مرتفعة، لأن أي خطأ في التخزين أو الصلاحيات أو منطق الترقية قد يكون مدمراً.

لهذا يجب التعامل مع الترقية كقرار معماري محسوب، لا كخيار افتراضي. وعندما تُبنى البنية بشكل صحيح باستخدام OpenZeppelin، وتُختبر عبر Hardhat، وتخضع لمراجعة أمنية جادة، تصبح العقود القابلة للترقية أداة قوية جداً لبناء تطبيقات Web3 أكثر نضجاً واستدامة.

4 comments

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *