استخدام نمط Proxy Contracts لفصل البيانات عن المنطق لتسهيل التحديثات المستقبلية
استخدام نمط Proxy Contracts لفصل البيانات عن المنطق لتسهيل التحديثات المستقبلية
عند نشر أي عقد ذكي على شبكة Blockchain يصبح عنوانه ثابتاً، لكن الكود نفسه غير قابل للتعديل مباشرة. هذه الميزة تمنح الثقة والشفافية، لكنها تخلق مشكلة عملية عندما تكتشف خطأ، أو تحتاج إلى إضافة خاصية جديدة، أو تحسين تجربة المستخدم في تطبيقات DApps.
لهذا ظهر نمط Proxy Contracts بوصفه حلاً معمارياً يفصل بين طبقة البيانات وطبقة المنطق. الفكرة ببساطة أن حالة العقد state تبقى داخل عقد ثابت، بينما يتم استبدال منطق التنفيذ عبر عقد آخر يسمى implementation.
إذا كنت قد قرأت مقال ترقية العقود الذكية (Upgradeable Contracts): كيف تحدث الكود بعد نشره على البلوكتشين؟ فهذا المقال يتوسع عملياً في القلب التقني لهذا المفهوم. كما أن فهم الفرق الحاسم بين Storage, Memory, و Calldata مهم جداً هنا، لأن الترقية الآمنة تعتمد أساساً على احترام تخطيط التخزين.
ما هو نمط Proxy ولماذا نحتاجه؟
في النموذج التقليدي، يحتوي العقد الواحد على المتغيرات والدوال معاً. عند الحاجة إلى تعديل منطق دالة مثل transfer() أو إضافة نظام صلاحيات جديد، لا يمكنك تعديل العقد المنشور، بل تضطر غالباً إلى نشر عقد جديد وترحيل البيانات أو مطالبة المستخدمين بالتعامل مع عنوان آخر.
أما في نمط Proxy، فإن المستخدم يتفاعل دائماً مع عنوان واحد ثابت. هذا العنوان يمثل العقد الوكيل، بينما يتم تحويل الاستدعاءات داخلياً إلى عقد المنطق باستخدام التعليمة منخفضة المستوى delegatecall.
أهمية delegatecall أنها تنفذ كود العقد الهدف، لكن ضمن مساحة تخزين العقد المستدعي. بمعنى آخر: الكود يأتي من عقد المنطق، أما البيانات فتظل محفوظة في عقد proxy. وهنا يتحقق الفصل الحقيقي بين البيانات والمنطق.
كيف تعمل البنية الداخلية لـ Proxy Contracts؟
1) عقد التخزين والتوجيه
العقد الوكيل يحتفظ بعنوان عقد المنطق الحالي داخل خانة تخزين خاصة، ويحتوي عادة على دالة fallback() تستقبل أي استدعاء غير معروف وتعيد توجيهه إلى عقد التنفيذ.
2) عقد المنطق
هذا العقد يضم الدوال الفعلية مثل عمليات الكتابة والقراءة، ويُكتب كما لو أنه العقد الرئيسي، لكنه في الواقع لا يحتفظ بالبيانات النهائية أثناء الاستخدام الفعلي. تخزين القيم يتم داخل الوكيل بسبب delegatecall.
3) عقد إدارة الترقية
في بعض الأنماط مثل Transparent Proxy أو UUPS توجد آلية مخصصة لتنفيذ الترقية بحيث لا يستطيع أي مستخدم عادي تغيير عنوان التنفيذ.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract CounterV1 {
uint256 public count;
address public owner;
bool private initialized;
function initialize(address _owner) external {
require(!initialized, "Already initialized");
owner = _owner;
initialized = true;
}
function increment() external {
count += 1;
}
}
contract SimpleProxy {
address public implementation;
address public admin;
constructor(address _implementation) {
implementation = _implementation;
admin = msg.sender;
}
function upgradeTo(address newImplementation) external {
require(msg.sender == admin, "Not admin");
implementation = newImplementation;
}
fallback() external payable {
address impl = implementation;
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
receive() external payable {}
}
هذا المثال مبسط للتعليم، لكنه يوضح الفكرة الجوهرية. العقد SimpleProxy يستقبل الاستدعاءات ويحولها إلى CounterV1. لكن عملياً لا يُنصح ببناء أنظمة ترقية يدوية في الإنتاج دون الاعتماد على مكتبة OpenZeppelin لكتابة عقود ذكية آمنة ومختبرة مسبقاً.
لماذا يعد تخطيط التخزين Storage Layout أخطر نقطة؟
عند ترقية المنطق من V1 إلى V2 يجب ألا تغيّر ترتيب متغيرات الحالة القديمة. لأن EVM يخزن القيم في فتحات slots مرتبة بدقة. أي تغيير عشوائي قد يجعل الرصيد يُقرأ كعنوان، أو يجعل متغير الصلاحية يطغى على قيمة أخرى.
القاعدة الذهبية هي: لا تحذف متغيراً قديماً، لا تعيد ترتيبه، ولا تغيّر نوعه. إذا احتجت حقولاً جديدة فأضفها في نهاية القائمة فقط. هذا مهم خصوصاً عند استخدام Mappings و Structs لأن بنيتها المنطقية مرتبطة مباشرة بطريقة التخزين.
تنبيه أمني: أخطر أخطاء الترقية ليست في منطق الدوال فقط، بل في كسر
storage layout. أي ترقية يجب أن تمر عبر مراجعة دقيقة، واختبارات مقارنة بين الإصدارات، وأدوات تحقق آلية قبل النشر.
أنماط الترقية الأشهر في بيئة Ethereum
Transparent Proxy
يفصل بين المستخدم العادي ومالك الترقية. إذا كان المستدعي هو المسؤول الإداري، فلن تمر بعض الاستدعاءات إلى التنفيذ لتجنب التضارب. هذا النمط واضح وسهل الفهم، ويستخدم كثيراً في المشاريع المؤسسية.
UUPS
ينقل منطق الترقية إلى عقد التنفيذ نفسه عبر دوال مثل upgradeTo(). يتميز بخفة أعلى في بعض الحالات وانخفاض نسبي في حجم الكود على الوكيل، لكنه يتطلب انضباطاً أمنياً أكبر.
Beacon Proxy
مفيد عندما تريد عدداً كبيراً من العقود الوكيلة تعتمد جميعها على عنوان تنفيذ واحد يمكن تغييره مركزياً من خلال beacon.
أفضل ممارسة عملية مع OpenZeppelin و Hardhat
في المشاريع الحقيقية، الأفضل استخدام إضافات الترقية الجاهزة بدلاً من كتابة assembly يدوياً. بعد إعداد بيئة العمل عبر تثبيت إطار عمل Hardhat باستخدام Node.js وإعداد مشروع Hardhat وكتابة أول سكربت JavaScript لترجمة العقد الذكي، يمكنك اعتماد مكتبات الترقية الرسمية.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
contract VaultV1 is Initializable {
uint256 public totalDeposits;
address public owner;
function initialize(address _owner) public initializer {
owner = _owner;
}
function deposit() external payable {
totalDeposits += msg.value;
}
}
لاحظ هنا أننا استخدمنا initialize() بدلاً من constructor. السبب أن العقد المنطقي خلف proxy لا ينفذ المُنشئ بالطريقة المعتادة أثناء العمل عبر الوكيل.
- استخدم
initializerمرة واحدة فقط. - أضف المتغيرات الجديدة دائماً في نهاية التخزين.
- اكتب اختبارات ترقية تقارن بيانات ما قبل وما بعد التحديث.
- راقب الأحداث باستخدام Events لتتبع عمليات الترقية على السلسلة.
تحسين غاز: نمط
proxyيضيف تكلفة طفيفة بسببdelegatecall. لذلك لا تستخدمه في كل عقد صغير بشكل عشوائي. قيّم أولاً هل العقد يحتاج فعلاً إلى ترقية مستقبلية أم أن البساطة وقلة الرسوم أولى.
أخطاء شائعة يجب تجنبها
- استخدام
constructorبدلاً منinitialize. - تغيير ترتيب متغيرات الحالة بين الإصدارات.
- نسيان تقييد صلاحية الترقية باستخدام Modifiers أو آليات حوكمة واضحة.
- عدم اختبار سيناريوهات الفشل باستخدام أدوات مثل Unit Tests باستخدام Chai & Mocha.
- إهمال تدقيق الأمان، خصوصاً عندما يحتوي العقد على تحويلات
Etherأو منطق حساس مشابه لما يُناقش في ثغرة إعادة الدخول Reentrancy.
الخلاصة
نمط Proxy Contracts ليس مجرد حيلة برمجية، بل هو طبقة معمارية أساسية في الأنظمة التي تتوقع نمواً مستمراً وتحديثات مستقبلية دون خسارة البيانات أو تغيير عنوان العقد أمام المستخدمين. قوته الحقيقية تكمن في الفصل بين التخزين والمنطق باستخدام delegatecall.
لكن هذه القوة تأتي مع مسؤولية كبيرة. أي مشروع يريد ترقية عقوده يجب أن يفهم جيداً تخطيط التخزين، أنماط الحوكمة، صلاحيات الترقية، واختبارات الأمان قبل النشر. وعند التنفيذ الصحيح، يصبح هذا النمط أداة ممتازة لبناء تطبيقات Web3 قابلة للصيانة، مرنة، وأكثر استعداداً للتطور على المدى الطويل.