استخدام نمط Proxy Contracts لفصل البيانات عن المنطق لتسهيل التحديثات المستقبلية

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

استخدام نمط Proxy Contracts لفصل البيانات عن المنطق لتسهيل التحديثات المستقبلية

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

فكرة النمط باختصار هي أن المستخدمين يتعاملون مع عقد وسيط ثابت العنوان، بينما يتم وضع منطق التنفيذ داخل عقد آخر قابل للاستبدال. بهذه الطريقة تبقى الحالة المخزنة State كما هي، في حين يمكن تحديث الكود التنفيذي مستقبلاً. هذا المفهوم مرتبط مباشرة بموضوع ترقية العقود الذكية (Upgradeable Contracts): كيف تحدث الكود بعد نشره على البلوكتشين؟.

إذا كنت قد بدأت رحلتك من مدخل إلى Web3: ما هو البلوكتشين ولماذا يغير شكل الإنترنت والأنظمة المالية؟ ثم تعلمت أساسيات أنواع البيانات والمتغيرات (State Variables) والدوال (Functions) في Solidity، فستجد أن نمط الترقية هذا هو الخطوة الطبيعية التالية لفهم كيفية بناء بروتوكولات طويلة العمر.

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

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

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

ما هو نمط Proxy عملياً؟

يتكون التصميم عادة من عنصرين رئيسيين:

  • عقد وسيط Proxy يحتفظ بالبيانات ويستقبل استدعاءات المستخدمين.
  • عقد تنفيذ Implementation أو Logic Contract يحتوي الدوال الفعلية.

عندما يستدعي المستخدم دالة مثل deposit() أو transfer()، يقوم الوسيط بتمرير الاستدعاء إلى عقد المنطق عبر delegatecall. الميزة المهمة هنا أن الكود المنفذ يأتي من عقد المنطق، لكن القراءة والكتابة تتم داخل تخزين عقد Proxy.

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

كيف يعمل delegatecall؟

تعليمة delegatecall تسمح لعقد بتنفيذ كود عقد آخر مع الاحتفاظ بسياق التنفيذ الأصلي. هذا يعني أن:

  1. العنوان msg.sender يبقى هو المستخدم الحقيقي.
  2. القيمة msg.value لا تتغير.
  3. جميع الكتابات على التخزين تتم في عقد Proxy وليس في عقد المنطق.

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

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

مثال مبسط على عقد منطق قابل للترقية

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

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

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

contract CounterV1 is Initializable, OwnableUpgradeable {
    uint256 public count;

    function initialize(address initialOwner) public initializer {
        __Ownable_init(initialOwner);
        count = 0;
    }

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

لاحظ أن استخدام constructor في العقود القابلة للترقية غير مناسب، لأن التهيئة تتم عبر العقد الوسيط. لذلك يتم استبداله بنمط initializer.

إصدار جديد يضيف منطقاً دون فقدان البيانات

عندما نحتاج إلى ميزة جديدة، ننشر نسخة ثانية من عقد المنطق ثم نحدث عنوان التنفيذ داخل Proxy. المثال التالي يضيف دالة إنقاص ويحافظ على نفس التخزين السابق.

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

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

contract CounterV2 is Initializable, OwnableUpgradeable {
    uint256 public count;

    function initialize(address initialOwner) public initializer {
        __Ownable_init(initialOwner);
        count = 0;
    }

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

    function decrement() external {
        require(count > 0, "Count cannot be negative");
        count -= 1;
    }
}

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

أشهر أنماط Upgradeable Proxies

1) نمط Transparent Proxy

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

2) نمط UUPS

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

3) نمط Beacon Proxy

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

أفضل الممارسات الأمنية عند بناء عقود Proxy

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

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

اختبار ونشر عقود الترقية عملياً

أفضل بيئة عملية لهذا النوع من المشاريع هي Hardhat مع إضافات OpenZeppelin Upgrades. وإذا كنت لم تجهز بيئة العمل بعد، فارجع إلى الانتقال إلى بيئة العمل الاحترافية: تثبيت إطار عمل Hardhat باستخدام Node.js ثم إعداد مشروع Hardhat وكتابة أول سكربت JavaScript لترجمة (Compile) العقد الذكي.

ولضمان سلامة التحديثات، يجب كتابة اختبارات تتحقق من:

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

هذا ينسجم مع ما تعلمته في اختبار العقود الذكية محلياً: كتابة اختبارات الوحدة (Unit Tests) باستخدام Chai & Mocha وأتمتة نشر العقود (Deployment): كتابة سكربت لرفع العقد إلى شبكة Ethereum و Polygon.

من منظور Gas Optimization، لا تجعل الترقية ذريعة لإضافة منطق متضخم داخل عقد واحد. فصل المسؤوليات وتقليل الكتابة إلى التخزين storage writes يساهم في خفض التكاليف، ويمكن تعميق هذه الفكرة عبر فهم التكاليف (Gas Fees): كيف يحسب البلوكتشين تكلفة تنفيذ الأكواد؟.

متى تستخدم هذا النمط ومتى تتجنبه؟

استخدم Proxy Contracts عندما تبني بروتوكولاً طويل الأجل، أو منتجاً سيتطور تدريجياً، أو نظاماً يحتاج إلى إصلاحات سريعة دون تغيير العنوان الذي تعتمد عليه الواجهات والتكاملات الخارجية.

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

خلاصة

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

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

3 comments

اترك تعليقاً

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